[{"data":1,"prerenderedAt":2603},["ShallowReactive",2],{"docs:\u002Fdocs\u002Fauthentication":3,"docs:nav":2554},{"id":4,"title":5,"badge":6,"body":7,"description":2546,"extension":2547,"meta":2548,"navigation":259,"order":243,"path":2549,"section":2550,"seo":2551,"stem":2552,"__hash__":2553},"docs\u002Fdocs\u002Fauthentication.md","Authentication",null,{"type":8,"value":9,"toc":2535},"minimark",[10,14,19,27,93,103,134,138,145,151,158,161,210,214,1207,1210,1401,1422,1426,1972,1982,1986,2272,2276,2281,2320,2324,2371,2381,2385,2392,2487,2502,2506,2516,2531],[11,12,13],"p",{},"Tessera has no sessions, no bearer tokens, and no cookies. Each request is independently signed and\nindependently expires. The scheme is small but unforgiving about details, so this page states them\nprecisely.",[15,16,18],"h2",{"id":17},"the-three-query-parameters","The three query parameters",[11,20,21,22,26],{},"Every authenticated request carries these as ",[23,24,25],"strong",{},"query parameters"," — not headers:",[28,29,30,43],"table",{},[31,32,33],"thead",{},[34,35,36,40],"tr",{},[37,38,39],"th",{},"Parameter",[37,41,42],{},"Value",[44,45,46,66,80],"tbody",{},[34,47,48,55],{},[49,50,51],"td",{},[52,53,54],"code",{},"credential",[49,56,57,58,61,62,65],{},"Your ed25519 ",[23,59,60],{},"public"," key, base64url encoded ",[23,63,64],{},"with padding",". This is also your account identifier.",[34,67,68,73],{},[49,69,70],{},[52,71,72],{},"validUntil",[49,74,75,76,79],{},"Unix timestamp in ",[23,77,78],{},"seconds"," at which the signature expires. You choose it.",[34,81,82,87],{},[49,83,84],{},[52,85,86],{},"signature",[49,88,89,90,92],{},"The ed25519 signature, base64url encoded ",[23,91,64],{},".",[94,95,100],"pre",{"className":96,"code":98,"language":99},[97],"language-text","POST \u002Fprepare-write?credential=Aq3f…%3D&validUntil=1775212800&signature=9Kd…%3D%3D\n","text",[52,101,98],{"__ignoreMap":102},"",[104,105,108],"app-callout",{"title":106,"tone":107},"Both encodings are padded, and it is checked","warn",[11,109,110,112,113,115,116,119,120,123,124,127,128,112,130,133],{},[52,111,54],{}," and ",[52,114,86],{}," are decoded with padded base64url. A 32-byte key produces a trailing\n",[52,117,118],{},"=","; a 64-byte signature produces a trailing ",[52,121,122],{},"==",". Strip the padding and you get\n",[52,125,126],{},"401 invalid credential",". Note also that the parameters must be URL-encoded in the query string,\nsince ",[52,129,118],{},[52,131,132],{},"+","-adjacent characters appear in base64url output.",[15,135,137],{"id":136},"what-gets-hashed","What gets hashed",[11,139,140,141,144],{},"The signature is over a ",[23,142,143],{},"blake2b-256"," digest — not SHA-256 — of five values concatenated with no\nseparators and no length prefixes:",[94,146,149],{"className":147,"code":148,"language":99},[97],"digest = blake2b256(\n    method            \u002F\u002F ASCII, e.g. \"POST\"\n  ‖ host              \u002F\u002F the HTTP Host header value, e.g. \"api.tessera.example\"\n  ‖ path              \u002F\u002F path only, e.g. \"\u002Fprepare-write\"\n  ‖ uint64le(validUntil)   \u002F\u002F 8 bytes, LITTLE-endian\n  ‖ body              \u002F\u002F raw request body bytes; omitted entirely if empty\n)\n\nsignature = ed25519_sign(privateKey, digest)\n",[52,150,148],{"__ignoreMap":102},[11,152,153,154,157],{},"Then verification is ",[52,155,156],{},"ed25519.Verify(publicKey, digest, signature)"," — the signature is over the\n32-byte digest, not over the message.",[11,159,160],{},"Three details that account for most failed integrations:",[162,163,164,183,194],"ol",{},[165,166,167,170,171,173,174,173,176,178,179,182],"li",{},[23,168,169],{},"The query string is not hashed."," Only the path. So ",[52,172,54],{},", ",[52,175,72],{},[52,177,86],{},",\nand any other query parameter such as ",[52,180,181],{},"?limit=50"," are outside the signature's protection.",[165,184,185,193],{},[23,186,187,189,190],{},[52,188,72],{}," is hashed as a little-endian ",[52,191,192],{},"uint64",", while being transmitted in the query\nstring as an ASCII decimal. These are different representations of the same number and both are\nrequired.",[165,195,196,206,207,209],{},[23,197,198,201,202,205],{},[52,199,200],{},"host"," must be byte-identical to the ",[52,203,204],{},"Host"," header the server receives"," — including the port\nif it is non-default. If you terminate TLS at a proxy that rewrites ",[52,208,204],{},", sign the value the\norigin sees, not the one you dialled.",[15,211,213],{"id":212},"typescript","TypeScript",[94,215,219],{"className":216,"code":217,"language":218,"meta":102,"style":102},"language-ts shiki shiki-themes github-dark-default github-dark-default","import { blake2b } from '@noble\u002Fhashes\u002Fblake2b'\nimport { ed25519 } from '@noble\u002Fcurves\u002Fed25519'\n\n\u002F** base64url WITH padding, as the API requires. *\u002F\nfunction b64urlPad(bytes: Uint8Array): string {\n  let s = ''\n  for (const b of bytes) s += String.fromCharCode(b)\n  return btoa(s).replace(\u002F\\+\u002Fg, '-').replace(\u002F\\\u002F\u002Fg, '_')\n}\n\nfunction b64urlDecode(s: string): Uint8Array {\n  const b64 = s.replace(\u002F-\u002Fg, '+').replace(\u002F_\u002Fg, '\u002F')\n  const pad = b64 + '='.repeat((4 - (b64.length % 4)) % 4)\n  return Uint8Array.from(atob(pad), (c) => c.charCodeAt(0))\n}\n\nexport interface Credential {\n  \u002F** appKey from POST \u002Fauth\u002Fprovision — base64url, unpadded, 64 bytes decoded. *\u002F\n  appKey: string\n  \u002F** credential from POST \u002Fauth\u002Fprovision — base64url, padded, 32 bytes decoded. *\u002F\n  credential: string\n}\n\nexport function signRequest(\n  cred: Credential,\n  method: string,\n  host: string,\n  path: string,\n  body: Uint8Array | null,\n  ttlSeconds = 60,\n): URLSearchParams {\n  const validUntil = Math.floor(Date.now() \u002F 1000) + ttlSeconds\n\n  const enc = new TextEncoder()\n  const le = new Uint8Array(8)\n  new DataView(le.buffer).setBigUint64(0, BigInt(validUntil), true) \u002F\u002F little-endian\n\n  const parts = [enc.encode(method), enc.encode(host), enc.encode(path), le]\n  if (body && body.length) parts.push(body)\n\n  const total = parts.reduce((n, p) => n + p.length, 0)\n  const message = new Uint8Array(total)\n  let off = 0\n  for (const p of parts) {\n    message.set(p, off)\n    off += p.length\n  }\n\n  const digest = blake2b(message, { dkLen: 32 })\n\n  \u002F\u002F appKey decodes to 64 bytes (seed ‖ public); ed25519 signing takes the 32-byte seed.\n  const seed = b64urlDecode(cred.appKey).slice(0, 32)\n  const signature = ed25519.sign(digest, seed)\n\n  return new URLSearchParams({\n    credential: cred.credential,\n    validUntil: String(validUntil),\n    signature: b64urlPad(signature),\n  })\n}\n","ts",[52,220,221,241,254,261,268,303,317,350,408,414,419,444,491,545,586,591,596,610,616,627,633,643,648,653,667,680,692,704,716,734,747,759,796,801,820,841,876,881,910,936,941,986,1003,1016,1033,1045,1058,1064,1069,1091,1096,1102,1130,1149,1154,1166,1172,1184,1196,1202],{"__ignoreMap":102},[222,223,226,230,234,237],"span",{"class":224,"line":225},"line",1,[222,227,229],{"class":228},"srH4v","import",[222,231,233],{"class":232},"sb7RM"," { blake2b } ",[222,235,236],{"class":228},"from",[222,238,240],{"class":239},"skZ57"," '@noble\u002Fhashes\u002Fblake2b'\n",[222,242,244,246,249,251],{"class":224,"line":243},2,[222,245,229],{"class":228},[222,247,248],{"class":232}," { ed25519 } ",[222,250,236],{"class":228},[222,252,253],{"class":239}," '@noble\u002Fcurves\u002Fed25519'\n",[222,255,257],{"class":224,"line":256},3,[222,258,260],{"emptyLinePlaceholder":259},true,"\n",[222,262,264],{"class":224,"line":263},4,[222,265,267],{"class":266},"sI3U9","\u002F** base64url WITH padding, as the API requires. *\u002F\n",[222,269,271,274,278,281,285,288,291,294,296,300],{"class":224,"line":270},5,[222,272,273],{"class":228},"function",[222,275,277],{"class":276},"sYGIp"," b64urlPad",[222,279,280],{"class":232},"(",[222,282,284],{"class":283},"sAxfE","bytes",[222,286,287],{"class":228},":",[222,289,290],{"class":283}," Uint8Array",[222,292,293],{"class":232},")",[222,295,287],{"class":228},[222,297,299],{"class":298},"sEQcL"," string",[222,301,302],{"class":232}," {\n",[222,304,306,309,312,314],{"class":224,"line":305},6,[222,307,308],{"class":228},"  let",[222,310,311],{"class":232}," s ",[222,313,118],{"class":228},[222,315,316],{"class":239}," ''\n",[222,318,320,323,326,329,332,335,338,341,344,347],{"class":224,"line":319},7,[222,321,322],{"class":228},"  for",[222,324,325],{"class":232}," (",[222,327,328],{"class":228},"const",[222,330,331],{"class":298}," b",[222,333,334],{"class":228}," of",[222,336,337],{"class":232}," bytes) s ",[222,339,340],{"class":228},"+=",[222,342,343],{"class":232}," String.",[222,345,346],{"class":276},"fromCharCode",[222,348,349],{"class":232},"(b)\n",[222,351,353,356,359,362,365,367,370,374,376,379,381,384,387,389,391,393,396,398,400,402,405],{"class":224,"line":352},8,[222,354,355],{"class":228},"  return",[222,357,358],{"class":276}," btoa",[222,360,361],{"class":232},"(s).",[222,363,364],{"class":276},"replace",[222,366,280],{"class":232},[222,368,369],{"class":239},"\u002F",[222,371,373],{"class":372},"sq3Sb","\\+",[222,375,369],{"class":239},[222,377,378],{"class":228},"g",[222,380,173],{"class":232},[222,382,383],{"class":239},"'-'",[222,385,386],{"class":232},").",[222,388,364],{"class":276},[222,390,280],{"class":232},[222,392,369],{"class":239},[222,394,395],{"class":372},"\\\u002F",[222,397,369],{"class":239},[222,399,378],{"class":228},[222,401,173],{"class":232},[222,403,404],{"class":239},"'_'",[222,406,407],{"class":232},")\n",[222,409,411],{"class":224,"line":410},9,[222,412,413],{"class":232},"}\n",[222,415,417],{"class":224,"line":416},10,[222,418,260],{"emptyLinePlaceholder":259},[222,420,422,424,427,429,432,434,436,438,440,442],{"class":224,"line":421},11,[222,423,273],{"class":228},[222,425,426],{"class":276}," b64urlDecode",[222,428,280],{"class":232},[222,430,431],{"class":283},"s",[222,433,287],{"class":228},[222,435,299],{"class":298},[222,437,293],{"class":232},[222,439,287],{"class":228},[222,441,290],{"class":283},[222,443,302],{"class":232},[222,445,447,450,453,456,459,461,463,466,468,470,473,475,477,479,482,484,486,489],{"class":224,"line":446},12,[222,448,449],{"class":228},"  const",[222,451,452],{"class":298}," b64",[222,454,455],{"class":228}," =",[222,457,458],{"class":232}," s.",[222,460,364],{"class":276},[222,462,280],{"class":232},[222,464,465],{"class":239},"\u002F-\u002F",[222,467,378],{"class":228},[222,469,173],{"class":232},[222,471,472],{"class":239},"'+'",[222,474,386],{"class":232},[222,476,364],{"class":276},[222,478,280],{"class":232},[222,480,481],{"class":239},"\u002F_\u002F",[222,483,378],{"class":228},[222,485,173],{"class":232},[222,487,488],{"class":239},"'\u002F'",[222,490,407],{"class":232},[222,492,494,496,499,501,504,506,509,511,514,517,520,523,526,529,532,535,538,541,543],{"class":224,"line":493},13,[222,495,449],{"class":228},[222,497,498],{"class":298}," pad",[222,500,455],{"class":228},[222,502,503],{"class":232}," b64 ",[222,505,132],{"class":228},[222,507,508],{"class":239}," '='",[222,510,92],{"class":232},[222,512,513],{"class":276},"repeat",[222,515,516],{"class":232},"((",[222,518,519],{"class":298},"4",[222,521,522],{"class":228}," -",[222,524,525],{"class":232}," (b64.",[222,527,528],{"class":298},"length",[222,530,531],{"class":228}," %",[222,533,534],{"class":298}," 4",[222,536,537],{"class":232},")) ",[222,539,540],{"class":228},"%",[222,542,534],{"class":298},[222,544,407],{"class":232},[222,546,548,550,553,555,557,560,563,566,569,572,575,578,580,583],{"class":224,"line":547},14,[222,549,355],{"class":228},[222,551,552],{"class":232}," Uint8Array.",[222,554,236],{"class":276},[222,556,280],{"class":232},[222,558,559],{"class":276},"atob",[222,561,562],{"class":232},"(pad), (",[222,564,565],{"class":283},"c",[222,567,568],{"class":232},") ",[222,570,571],{"class":228},"=>",[222,573,574],{"class":232}," c.",[222,576,577],{"class":276},"charCodeAt",[222,579,280],{"class":232},[222,581,582],{"class":298},"0",[222,584,585],{"class":232},"))\n",[222,587,589],{"class":224,"line":588},15,[222,590,413],{"class":232},[222,592,594],{"class":224,"line":593},16,[222,595,260],{"emptyLinePlaceholder":259},[222,597,599,602,605,608],{"class":224,"line":598},17,[222,600,601],{"class":228},"export",[222,603,604],{"class":228}," interface",[222,606,607],{"class":283}," Credential",[222,609,302],{"class":232},[222,611,613],{"class":224,"line":612},18,[222,614,615],{"class":266},"  \u002F** appKey from POST \u002Fauth\u002Fprovision — base64url, unpadded, 64 bytes decoded. *\u002F\n",[222,617,619,622,624],{"class":224,"line":618},19,[222,620,621],{"class":283},"  appKey",[222,623,287],{"class":228},[222,625,626],{"class":298}," string\n",[222,628,630],{"class":224,"line":629},20,[222,631,632],{"class":266},"  \u002F** credential from POST \u002Fauth\u002Fprovision — base64url, padded, 32 bytes decoded. *\u002F\n",[222,634,636,639,641],{"class":224,"line":635},21,[222,637,638],{"class":283},"  credential",[222,640,287],{"class":228},[222,642,626],{"class":298},[222,644,646],{"class":224,"line":645},22,[222,647,413],{"class":232},[222,649,651],{"class":224,"line":650},23,[222,652,260],{"emptyLinePlaceholder":259},[222,654,656,658,661,664],{"class":224,"line":655},24,[222,657,601],{"class":228},[222,659,660],{"class":228}," function",[222,662,663],{"class":276}," signRequest",[222,665,666],{"class":232},"(\n",[222,668,670,673,675,677],{"class":224,"line":669},25,[222,671,672],{"class":283},"  cred",[222,674,287],{"class":228},[222,676,607],{"class":283},[222,678,679],{"class":232},",\n",[222,681,683,686,688,690],{"class":224,"line":682},26,[222,684,685],{"class":283},"  method",[222,687,287],{"class":228},[222,689,299],{"class":298},[222,691,679],{"class":232},[222,693,695,698,700,702],{"class":224,"line":694},27,[222,696,697],{"class":283},"  host",[222,699,287],{"class":228},[222,701,299],{"class":298},[222,703,679],{"class":232},[222,705,707,710,712,714],{"class":224,"line":706},28,[222,708,709],{"class":283},"  path",[222,711,287],{"class":228},[222,713,299],{"class":298},[222,715,679],{"class":232},[222,717,719,722,724,726,729,732],{"class":224,"line":718},29,[222,720,721],{"class":283},"  body",[222,723,287],{"class":228},[222,725,290],{"class":283},[222,727,728],{"class":228}," |",[222,730,731],{"class":298}," null",[222,733,679],{"class":232},[222,735,737,740,742,745],{"class":224,"line":736},30,[222,738,739],{"class":283},"  ttlSeconds",[222,741,455],{"class":228},[222,743,744],{"class":298}," 60",[222,746,679],{"class":232},[222,748,750,752,754,757],{"class":224,"line":749},31,[222,751,293],{"class":232},[222,753,287],{"class":228},[222,755,756],{"class":283}," URLSearchParams",[222,758,302],{"class":232},[222,760,762,764,767,769,772,775,778,781,784,786,789,791,793],{"class":224,"line":761},32,[222,763,449],{"class":228},[222,765,766],{"class":298}," validUntil",[222,768,455],{"class":228},[222,770,771],{"class":232}," Math.",[222,773,774],{"class":276},"floor",[222,776,777],{"class":232},"(Date.",[222,779,780],{"class":276},"now",[222,782,783],{"class":232},"() ",[222,785,369],{"class":228},[222,787,788],{"class":298}," 1000",[222,790,568],{"class":232},[222,792,132],{"class":228},[222,794,795],{"class":232}," ttlSeconds\n",[222,797,799],{"class":224,"line":798},33,[222,800,260],{"emptyLinePlaceholder":259},[222,802,804,806,809,811,814,817],{"class":224,"line":803},34,[222,805,449],{"class":228},[222,807,808],{"class":298}," enc",[222,810,455],{"class":228},[222,812,813],{"class":228}," new",[222,815,816],{"class":276}," TextEncoder",[222,818,819],{"class":232},"()\n",[222,821,823,825,828,830,832,834,836,839],{"class":224,"line":822},35,[222,824,449],{"class":228},[222,826,827],{"class":298}," le",[222,829,455],{"class":228},[222,831,813],{"class":228},[222,833,290],{"class":276},[222,835,280],{"class":232},[222,837,838],{"class":298},"8",[222,840,407],{"class":232},[222,842,844,847,850,853,856,858,860,862,865,868,871,873],{"class":224,"line":843},36,[222,845,846],{"class":228},"  new",[222,848,849],{"class":276}," DataView",[222,851,852],{"class":232},"(le.buffer).",[222,854,855],{"class":276},"setBigUint64",[222,857,280],{"class":232},[222,859,582],{"class":298},[222,861,173],{"class":232},[222,863,864],{"class":276},"BigInt",[222,866,867],{"class":232},"(validUntil), ",[222,869,870],{"class":298},"true",[222,872,568],{"class":232},[222,874,875],{"class":266},"\u002F\u002F little-endian\n",[222,877,879],{"class":224,"line":878},37,[222,880,260],{"emptyLinePlaceholder":259},[222,882,884,886,889,891,894,897,900,902,905,907],{"class":224,"line":883},38,[222,885,449],{"class":228},[222,887,888],{"class":298}," parts",[222,890,455],{"class":228},[222,892,893],{"class":232}," [enc.",[222,895,896],{"class":276},"encode",[222,898,899],{"class":232},"(method), enc.",[222,901,896],{"class":276},[222,903,904],{"class":232},"(host), enc.",[222,906,896],{"class":276},[222,908,909],{"class":232},"(path), le]\n",[222,911,913,916,919,922,925,927,930,933],{"class":224,"line":912},39,[222,914,915],{"class":228},"  if",[222,917,918],{"class":232}," (body ",[222,920,921],{"class":228},"&&",[222,923,924],{"class":232}," body.",[222,926,528],{"class":298},[222,928,929],{"class":232},") parts.",[222,931,932],{"class":276},"push",[222,934,935],{"class":232},"(body)\n",[222,937,939],{"class":224,"line":938},40,[222,940,260],{"emptyLinePlaceholder":259},[222,942,944,946,949,951,954,957,959,962,964,966,968,970,973,975,978,980,982,984],{"class":224,"line":943},41,[222,945,449],{"class":228},[222,947,948],{"class":298}," total",[222,950,455],{"class":228},[222,952,953],{"class":232}," parts.",[222,955,956],{"class":276},"reduce",[222,958,516],{"class":232},[222,960,961],{"class":283},"n",[222,963,173],{"class":232},[222,965,11],{"class":283},[222,967,568],{"class":232},[222,969,571],{"class":228},[222,971,972],{"class":232}," n ",[222,974,132],{"class":228},[222,976,977],{"class":232}," p.",[222,979,528],{"class":298},[222,981,173],{"class":232},[222,983,582],{"class":298},[222,985,407],{"class":232},[222,987,989,991,994,996,998,1000],{"class":224,"line":988},42,[222,990,449],{"class":228},[222,992,993],{"class":298}," message",[222,995,455],{"class":228},[222,997,813],{"class":228},[222,999,290],{"class":276},[222,1001,1002],{"class":232},"(total)\n",[222,1004,1006,1008,1011,1013],{"class":224,"line":1005},43,[222,1007,308],{"class":228},[222,1009,1010],{"class":232}," off ",[222,1012,118],{"class":228},[222,1014,1015],{"class":298}," 0\n",[222,1017,1019,1021,1023,1025,1028,1030],{"class":224,"line":1018},44,[222,1020,322],{"class":228},[222,1022,325],{"class":232},[222,1024,328],{"class":228},[222,1026,1027],{"class":298}," p",[222,1029,334],{"class":228},[222,1031,1032],{"class":232}," parts) {\n",[222,1034,1036,1039,1042],{"class":224,"line":1035},45,[222,1037,1038],{"class":232},"    message.",[222,1040,1041],{"class":276},"set",[222,1043,1044],{"class":232},"(p, off)\n",[222,1046,1048,1051,1053,1055],{"class":224,"line":1047},46,[222,1049,1050],{"class":232},"    off ",[222,1052,340],{"class":228},[222,1054,977],{"class":232},[222,1056,1057],{"class":298},"length\n",[222,1059,1061],{"class":224,"line":1060},47,[222,1062,1063],{"class":232},"  }\n",[222,1065,1067],{"class":224,"line":1066},48,[222,1068,260],{"emptyLinePlaceholder":259},[222,1070,1072,1074,1077,1079,1082,1085,1088],{"class":224,"line":1071},49,[222,1073,449],{"class":228},[222,1075,1076],{"class":298}," digest",[222,1078,455],{"class":228},[222,1080,1081],{"class":276}," blake2b",[222,1083,1084],{"class":232},"(message, { dkLen: ",[222,1086,1087],{"class":298},"32",[222,1089,1090],{"class":232}," })\n",[222,1092,1094],{"class":224,"line":1093},50,[222,1095,260],{"emptyLinePlaceholder":259},[222,1097,1099],{"class":224,"line":1098},51,[222,1100,1101],{"class":266},"  \u002F\u002F appKey decodes to 64 bytes (seed ‖ public); ed25519 signing takes the 32-byte seed.\n",[222,1103,1105,1107,1110,1112,1114,1117,1120,1122,1124,1126,1128],{"class":224,"line":1104},52,[222,1106,449],{"class":228},[222,1108,1109],{"class":298}," seed",[222,1111,455],{"class":228},[222,1113,426],{"class":276},[222,1115,1116],{"class":232},"(cred.appKey).",[222,1118,1119],{"class":276},"slice",[222,1121,280],{"class":232},[222,1123,582],{"class":298},[222,1125,173],{"class":232},[222,1127,1087],{"class":298},[222,1129,407],{"class":232},[222,1131,1133,1135,1138,1140,1143,1146],{"class":224,"line":1132},53,[222,1134,449],{"class":228},[222,1136,1137],{"class":298}," signature",[222,1139,455],{"class":228},[222,1141,1142],{"class":232}," ed25519.",[222,1144,1145],{"class":276},"sign",[222,1147,1148],{"class":232},"(digest, seed)\n",[222,1150,1152],{"class":224,"line":1151},54,[222,1153,260],{"emptyLinePlaceholder":259},[222,1155,1157,1159,1161,1163],{"class":224,"line":1156},55,[222,1158,355],{"class":228},[222,1160,813],{"class":228},[222,1162,756],{"class":276},[222,1164,1165],{"class":232},"({\n",[222,1167,1169],{"class":224,"line":1168},56,[222,1170,1171],{"class":232},"    credential: cred.credential,\n",[222,1173,1175,1178,1181],{"class":224,"line":1174},57,[222,1176,1177],{"class":232},"    validUntil: ",[222,1179,1180],{"class":276},"String",[222,1182,1183],{"class":232},"(validUntil),\n",[222,1185,1187,1190,1193],{"class":224,"line":1186},58,[222,1188,1189],{"class":232},"    signature: ",[222,1191,1192],{"class":276},"b64urlPad",[222,1194,1195],{"class":232},"(signature),\n",[222,1197,1199],{"class":224,"line":1198},59,[222,1200,1201],{"class":232},"  })\n",[222,1203,1205],{"class":224,"line":1204},60,[222,1206,413],{"class":232},[11,1208,1209],{},"Using it:",[94,1211,1213],{"className":216,"code":1212,"language":218,"meta":102,"style":102},"const base = new URL('https:\u002F\u002Fapi.PLACEHOLDER_DOMAIN.example')\nconst path = '\u002Fprepare-write'\nconst body = new TextEncoder().encode(JSON.stringify({ totalShards: 15, minShards: 10 }))\n\nconst params = signRequest(cred, 'POST', base.host, path, body)\n\nconst res = await fetch(`${base.origin}${path}?${params}`, {\n  method: 'POST',\n  headers: { 'content-type': 'application\u002Fjson' },\n  body,\n})\n",[52,1214,1215,1236,1248,1291,1295,1315,1319,1365,1374,1391,1396],{"__ignoreMap":102},[222,1216,1217,1219,1222,1224,1226,1229,1231,1234],{"class":224,"line":225},[222,1218,328],{"class":228},[222,1220,1221],{"class":298}," base",[222,1223,455],{"class":228},[222,1225,813],{"class":228},[222,1227,1228],{"class":276}," URL",[222,1230,280],{"class":232},[222,1232,1233],{"class":239},"'https:\u002F\u002Fapi.PLACEHOLDER_DOMAIN.example'",[222,1235,407],{"class":232},[222,1237,1238,1240,1243,1245],{"class":224,"line":243},[222,1239,328],{"class":228},[222,1241,1242],{"class":298}," path",[222,1244,455],{"class":228},[222,1246,1247],{"class":239}," '\u002Fprepare-write'\n",[222,1249,1250,1252,1255,1257,1259,1261,1264,1266,1268,1271,1273,1276,1279,1282,1285,1288],{"class":224,"line":256},[222,1251,328],{"class":228},[222,1253,1254],{"class":298}," body",[222,1256,455],{"class":228},[222,1258,813],{"class":228},[222,1260,816],{"class":276},[222,1262,1263],{"class":232},"().",[222,1265,896],{"class":276},[222,1267,280],{"class":232},[222,1269,1270],{"class":298},"JSON",[222,1272,92],{"class":232},[222,1274,1275],{"class":276},"stringify",[222,1277,1278],{"class":232},"({ totalShards: ",[222,1280,1281],{"class":298},"15",[222,1283,1284],{"class":232},", minShards: ",[222,1286,1287],{"class":298},"10",[222,1289,1290],{"class":232}," }))\n",[222,1292,1293],{"class":224,"line":263},[222,1294,260],{"emptyLinePlaceholder":259},[222,1296,1297,1299,1302,1304,1306,1309,1312],{"class":224,"line":270},[222,1298,328],{"class":228},[222,1300,1301],{"class":298}," params",[222,1303,455],{"class":228},[222,1305,663],{"class":276},[222,1307,1308],{"class":232},"(cred, ",[222,1310,1311],{"class":239},"'POST'",[222,1313,1314],{"class":232},", base.host, path, body)\n",[222,1316,1317],{"class":224,"line":305},[222,1318,260],{"emptyLinePlaceholder":259},[222,1320,1321,1323,1326,1328,1331,1334,1336,1339,1342,1344,1347,1350,1353,1356,1359,1362],{"class":224,"line":319},[222,1322,328],{"class":228},[222,1324,1325],{"class":298}," res",[222,1327,455],{"class":228},[222,1329,1330],{"class":228}," await",[222,1332,1333],{"class":276}," fetch",[222,1335,280],{"class":232},[222,1337,1338],{"class":239},"`${",[222,1340,1341],{"class":232},"base",[222,1343,92],{"class":239},[222,1345,1346],{"class":232},"origin",[222,1348,1349],{"class":239},"}${",[222,1351,1352],{"class":232},"path",[222,1354,1355],{"class":239},"}?${",[222,1357,1358],{"class":232},"params",[222,1360,1361],{"class":239},"}`",[222,1363,1364],{"class":232},", {\n",[222,1366,1367,1370,1372],{"class":224,"line":352},[222,1368,1369],{"class":232},"  method: ",[222,1371,1311],{"class":239},[222,1373,679],{"class":232},[222,1375,1376,1379,1382,1385,1388],{"class":224,"line":410},[222,1377,1378],{"class":232},"  headers: { ",[222,1380,1381],{"class":239},"'content-type'",[222,1383,1384],{"class":232},": ",[222,1386,1387],{"class":239},"'application\u002Fjson'",[222,1389,1390],{"class":232}," },\n",[222,1392,1393],{"class":224,"line":416},[222,1394,1395],{"class":232},"  body,\n",[222,1397,1398],{"class":224,"line":421},[222,1399,1400],{"class":232},"})\n",[104,1402,1405],{"title":1403,"tone":1404},"Note the `host` value","note",[11,1406,1407,1410,1411,1414,1415,1417,1418,1421],{},[52,1408,1409],{},"base.host"," gives ",[52,1412,1413],{},"api.tessera.example"," — hostname plus port when present, without the scheme.\nThat is exactly what the ",[52,1416,204],{}," header carries and exactly what must be hashed. ",[52,1419,1420],{},"base.hostname","\nwould drop the port and break signing against a non-default port.",[15,1423,1425],{"id":1424},"go","Go",[94,1427,1430],{"className":1428,"code":1429,"language":1424,"meta":102,"style":102},"language-go shiki shiki-themes github-dark-default github-dark-default","package tessera\n\nimport (\n    \"crypto\u002Fed25519\"\n    \"encoding\u002Fbase64\"\n    \"encoding\u002Fbinary\"\n    \"net\u002Furl\"\n    \"strconv\"\n    \"time\"\n\n    \"golang.org\u002Fx\u002Fcrypto\u002Fblake2b\"\n)\n\n\u002F\u002F SignParams returns the credential, validUntil, and signature query parameters.\nfunc SignParams(appKey, credential, method, host, path string, body []byte, ttl time.Duration) (url.Values, error) {\n    priv, err := base64.RawURLEncoding.DecodeString(appKey)\n    if err != nil {\n        return nil, err\n    }\n\n    validUntil := time.Now().Add(ttl).Unix()\n\n    h, err := blake2b.New256(nil)\n    if err != nil {\n        return nil, err\n    }\n    h.Write([]byte(method))\n    h.Write([]byte(host))\n    h.Write([]byte(path))\n\n    var le [8]byte\n    binary.LittleEndian.PutUint64(le[:], uint64(validUntil))\n    h.Write(le[:])\n\n    if len(body) > 0 {\n        h.Write(body)\n    }\n    digest := h.Sum(nil)\n\n    sig := ed25519.Sign(ed25519.PrivateKey(priv), digest)\n\n    return url.Values{\n        \"credential\": {credential},\n        \"validUntil\": {strconv.FormatInt(validUntil, 10)},\n        \"signature\":  {base64.URLEncoding.EncodeToString(sig)},\n    }, nil\n}\n",[52,1431,1432,1440,1444,1451,1462,1471,1480,1489,1498,1507,1511,1520,1524,1528,1533,1608,1625,1641,1651,1656,1660,1686,1690,1710,1722,1730,1734,1750,1763,1776,1780,1796,1812,1821,1825,1843,1852,1856,1875,1879,1900,1904,1919,1927,1946,1960,1968],{"__ignoreMap":102},[222,1433,1434,1437],{"class":224,"line":225},[222,1435,1436],{"class":228},"package",[222,1438,1439],{"class":283}," tessera\n",[222,1441,1442],{"class":224,"line":243},[222,1443,260],{"emptyLinePlaceholder":259},[222,1445,1446,1448],{"class":224,"line":256},[222,1447,229],{"class":228},[222,1449,1450],{"class":232}," (\n",[222,1452,1453,1456,1459],{"class":224,"line":263},[222,1454,1455],{"class":239},"    \"",[222,1457,1458],{"class":283},"crypto\u002Fed25519",[222,1460,1461],{"class":239},"\"\n",[222,1463,1464,1466,1469],{"class":224,"line":270},[222,1465,1455],{"class":239},[222,1467,1468],{"class":283},"encoding\u002Fbase64",[222,1470,1461],{"class":239},[222,1472,1473,1475,1478],{"class":224,"line":305},[222,1474,1455],{"class":239},[222,1476,1477],{"class":283},"encoding\u002Fbinary",[222,1479,1461],{"class":239},[222,1481,1482,1484,1487],{"class":224,"line":319},[222,1483,1455],{"class":239},[222,1485,1486],{"class":283},"net\u002Furl",[222,1488,1461],{"class":239},[222,1490,1491,1493,1496],{"class":224,"line":352},[222,1492,1455],{"class":239},[222,1494,1495],{"class":283},"strconv",[222,1497,1461],{"class":239},[222,1499,1500,1502,1505],{"class":224,"line":410},[222,1501,1455],{"class":239},[222,1503,1504],{"class":283},"time",[222,1506,1461],{"class":239},[222,1508,1509],{"class":224,"line":416},[222,1510,260],{"emptyLinePlaceholder":259},[222,1512,1513,1515,1518],{"class":224,"line":421},[222,1514,1455],{"class":239},[222,1516,1517],{"class":283},"golang.org\u002Fx\u002Fcrypto\u002Fblake2b",[222,1519,1461],{"class":239},[222,1521,1522],{"class":224,"line":446},[222,1523,407],{"class":232},[222,1525,1526],{"class":224,"line":493},[222,1527,260],{"emptyLinePlaceholder":259},[222,1529,1530],{"class":224,"line":547},[222,1531,1532],{"class":266},"\u002F\u002F SignParams returns the credential, validUntil, and signature query parameters.\n",[222,1534,1535,1538,1541,1543,1546,1548,1550,1552,1555,1557,1559,1561,1563,1565,1567,1570,1573,1576,1578,1581,1584,1586,1589,1592,1595,1597,1600,1602,1605],{"class":224,"line":588},[222,1536,1537],{"class":228},"func",[222,1539,1540],{"class":276}," SignParams",[222,1542,280],{"class":232},[222,1544,1545],{"class":283},"appKey",[222,1547,173],{"class":232},[222,1549,54],{"class":283},[222,1551,173],{"class":232},[222,1553,1554],{"class":283},"method",[222,1556,173],{"class":232},[222,1558,200],{"class":283},[222,1560,173],{"class":232},[222,1562,1352],{"class":283},[222,1564,299],{"class":228},[222,1566,173],{"class":232},[222,1568,1569],{"class":283},"body",[222,1571,1572],{"class":232}," []",[222,1574,1575],{"class":228},"byte",[222,1577,173],{"class":232},[222,1579,1580],{"class":283},"ttl",[222,1582,1583],{"class":283}," time",[222,1585,92],{"class":232},[222,1587,1588],{"class":283},"Duration",[222,1590,1591],{"class":232},") (",[222,1593,1594],{"class":283},"url",[222,1596,92],{"class":232},[222,1598,1599],{"class":283},"Values",[222,1601,173],{"class":232},[222,1603,1604],{"class":228},"error",[222,1606,1607],{"class":232},") {\n",[222,1609,1610,1613,1616,1619,1622],{"class":224,"line":593},[222,1611,1612],{"class":232},"    priv, err ",[222,1614,1615],{"class":228},":=",[222,1617,1618],{"class":232}," base64.RawURLEncoding.",[222,1620,1621],{"class":276},"DecodeString",[222,1623,1624],{"class":232},"(appKey)\n",[222,1626,1627,1630,1633,1636,1639],{"class":224,"line":598},[222,1628,1629],{"class":228},"    if",[222,1631,1632],{"class":232}," err ",[222,1634,1635],{"class":228},"!=",[222,1637,1638],{"class":298}," nil",[222,1640,302],{"class":232},[222,1642,1643,1646,1648],{"class":224,"line":612},[222,1644,1645],{"class":228},"        return",[222,1647,1638],{"class":298},[222,1649,1650],{"class":232},", err\n",[222,1652,1653],{"class":224,"line":618},[222,1654,1655],{"class":232},"    }\n",[222,1657,1658],{"class":224,"line":629},[222,1659,260],{"emptyLinePlaceholder":259},[222,1661,1662,1665,1667,1670,1673,1675,1678,1681,1684],{"class":224,"line":635},[222,1663,1664],{"class":232},"    validUntil ",[222,1666,1615],{"class":228},[222,1668,1669],{"class":232}," time.",[222,1671,1672],{"class":276},"Now",[222,1674,1263],{"class":232},[222,1676,1677],{"class":276},"Add",[222,1679,1680],{"class":232},"(ttl).",[222,1682,1683],{"class":276},"Unix",[222,1685,819],{"class":232},[222,1687,1688],{"class":224,"line":645},[222,1689,260],{"emptyLinePlaceholder":259},[222,1691,1692,1695,1697,1700,1703,1705,1708],{"class":224,"line":650},[222,1693,1694],{"class":232},"    h, err ",[222,1696,1615],{"class":228},[222,1698,1699],{"class":232}," blake2b.",[222,1701,1702],{"class":276},"New256",[222,1704,280],{"class":232},[222,1706,1707],{"class":298},"nil",[222,1709,407],{"class":232},[222,1711,1712,1714,1716,1718,1720],{"class":224,"line":655},[222,1713,1629],{"class":228},[222,1715,1632],{"class":232},[222,1717,1635],{"class":228},[222,1719,1638],{"class":298},[222,1721,302],{"class":232},[222,1723,1724,1726,1728],{"class":224,"line":669},[222,1725,1645],{"class":228},[222,1727,1638],{"class":298},[222,1729,1650],{"class":232},[222,1731,1732],{"class":224,"line":682},[222,1733,1655],{"class":232},[222,1735,1736,1739,1742,1745,1747],{"class":224,"line":694},[222,1737,1738],{"class":232},"    h.",[222,1740,1741],{"class":276},"Write",[222,1743,1744],{"class":232},"([]",[222,1746,1575],{"class":228},[222,1748,1749],{"class":232},"(method))\n",[222,1751,1752,1754,1756,1758,1760],{"class":224,"line":706},[222,1753,1738],{"class":232},[222,1755,1741],{"class":276},[222,1757,1744],{"class":232},[222,1759,1575],{"class":228},[222,1761,1762],{"class":232},"(host))\n",[222,1764,1765,1767,1769,1771,1773],{"class":224,"line":718},[222,1766,1738],{"class":232},[222,1768,1741],{"class":276},[222,1770,1744],{"class":232},[222,1772,1575],{"class":228},[222,1774,1775],{"class":232},"(path))\n",[222,1777,1778],{"class":224,"line":736},[222,1779,260],{"emptyLinePlaceholder":259},[222,1781,1782,1785,1788,1790,1793],{"class":224,"line":749},[222,1783,1784],{"class":228},"    var",[222,1786,1787],{"class":232}," le [",[222,1789,838],{"class":298},[222,1791,1792],{"class":232},"]",[222,1794,1795],{"class":228},"byte\n",[222,1797,1798,1801,1804,1807,1809],{"class":224,"line":761},[222,1799,1800],{"class":232},"    binary.LittleEndian.",[222,1802,1803],{"class":276},"PutUint64",[222,1805,1806],{"class":232},"(le[:], ",[222,1808,192],{"class":228},[222,1810,1811],{"class":232},"(validUntil))\n",[222,1813,1814,1816,1818],{"class":224,"line":798},[222,1815,1738],{"class":232},[222,1817,1741],{"class":276},[222,1819,1820],{"class":232},"(le[:])\n",[222,1822,1823],{"class":224,"line":803},[222,1824,260],{"emptyLinePlaceholder":259},[222,1826,1827,1829,1832,1835,1838,1841],{"class":224,"line":822},[222,1828,1629],{"class":228},[222,1830,1831],{"class":276}," len",[222,1833,1834],{"class":232},"(body) ",[222,1836,1837],{"class":228},">",[222,1839,1840],{"class":298}," 0",[222,1842,302],{"class":232},[222,1844,1845,1848,1850],{"class":224,"line":843},[222,1846,1847],{"class":232},"        h.",[222,1849,1741],{"class":276},[222,1851,935],{"class":232},[222,1853,1854],{"class":224,"line":878},[222,1855,1655],{"class":232},[222,1857,1858,1861,1863,1866,1869,1871,1873],{"class":224,"line":883},[222,1859,1860],{"class":232},"    digest ",[222,1862,1615],{"class":228},[222,1864,1865],{"class":232}," h.",[222,1867,1868],{"class":276},"Sum",[222,1870,280],{"class":232},[222,1872,1707],{"class":298},[222,1874,407],{"class":232},[222,1876,1877],{"class":224,"line":912},[222,1878,260],{"emptyLinePlaceholder":259},[222,1880,1881,1884,1886,1888,1891,1894,1897],{"class":224,"line":938},[222,1882,1883],{"class":232},"    sig ",[222,1885,1615],{"class":228},[222,1887,1142],{"class":232},[222,1889,1890],{"class":276},"Sign",[222,1892,1893],{"class":232},"(ed25519.",[222,1895,1896],{"class":276},"PrivateKey",[222,1898,1899],{"class":232},"(priv), digest)\n",[222,1901,1902],{"class":224,"line":943},[222,1903,260],{"emptyLinePlaceholder":259},[222,1905,1906,1909,1912,1914,1916],{"class":224,"line":988},[222,1907,1908],{"class":228},"    return",[222,1910,1911],{"class":283}," url",[222,1913,92],{"class":232},[222,1915,1599],{"class":283},[222,1917,1918],{"class":232},"{\n",[222,1920,1921,1924],{"class":224,"line":1005},[222,1922,1923],{"class":239},"        \"credential\"",[222,1925,1926],{"class":232},": {credential},\n",[222,1928,1929,1932,1935,1938,1941,1943],{"class":224,"line":1018},[222,1930,1931],{"class":239},"        \"validUntil\"",[222,1933,1934],{"class":232},": {strconv.",[222,1936,1937],{"class":276},"FormatInt",[222,1939,1940],{"class":232},"(validUntil, ",[222,1942,1287],{"class":298},[222,1944,1945],{"class":232},")},\n",[222,1947,1948,1951,1954,1957],{"class":224,"line":1035},[222,1949,1950],{"class":239},"        \"signature\"",[222,1952,1953],{"class":232},":  {base64.URLEncoding.",[222,1955,1956],{"class":276},"EncodeToString",[222,1958,1959],{"class":232},"(sig)},\n",[222,1961,1962,1965],{"class":224,"line":1047},[222,1963,1964],{"class":232},"    }, ",[222,1966,1967],{"class":298},"nil\n",[222,1969,1970],{"class":224,"line":1060},[222,1971,413],{"class":232},[11,1973,1974,1975,173,1978,1981],{},"If you already depend on ",[52,1976,1977],{},"go.sia.tech\u002Fcore",[52,1979,1980],{},"types.NewHasher()"," produces the identical digest —\nit is blake2b-256 with a raw-byte encoder, which is what the construction above reproduces by hand.",[15,1983,1985],{"id":1984},"python","Python",[94,1987,1990],{"className":1988,"code":1989,"language":1984,"meta":102,"style":102},"language-python shiki shiki-themes github-dark-default github-dark-default","import base64, hashlib, struct, time\nfrom nacl.signing import SigningKey\n\ndef sign_params(app_key: str, credential: str, method: str, host: str,\n                path: str, body: bytes = b\"\", ttl: int = 60) -> dict:\n    valid_until = int(time.time()) + ttl\n\n    priv = base64.urlsafe_b64decode(app_key + \"=\" * (-len(app_key) % 4))\n    seed = priv[:32]\n\n    h = hashlib.blake2b(digest_size=32)\n    h.update(method.encode())\n    h.update(host.encode())\n    h.update(path.encode())\n    h.update(struct.pack(\"\u003CQ\", valid_until))   # little-endian uint64\n    if body:\n        h.update(body)\n\n    sig = SigningKey(seed).sign(h.digest()).signature\n\n    return {\n        \"credential\": credential,\n        \"validUntil\": str(valid_until),\n        \"signature\": base64.urlsafe_b64encode(sig).decode(),\n    }\n",[52,1991,1992,1999,2011,2015,2046,2084,2102,2106,2141,2156,2160,2179,2184,2189,2194,2208,2215,2220,2224,2233,2237,2243,2250,2261,2268],{"__ignoreMap":102},[222,1993,1994,1996],{"class":224,"line":225},[222,1995,229],{"class":228},[222,1997,1998],{"class":232}," base64, hashlib, struct, time\n",[222,2000,2001,2003,2006,2008],{"class":224,"line":243},[222,2002,236],{"class":228},[222,2004,2005],{"class":232}," nacl.signing ",[222,2007,229],{"class":228},[222,2009,2010],{"class":232}," SigningKey\n",[222,2012,2013],{"class":224,"line":256},[222,2014,260],{"emptyLinePlaceholder":259},[222,2016,2017,2020,2023,2026,2029,2032,2034,2037,2039,2042,2044],{"class":224,"line":263},[222,2018,2019],{"class":228},"def",[222,2021,2022],{"class":276}," sign_params",[222,2024,2025],{"class":232},"(app_key: ",[222,2027,2028],{"class":298},"str",[222,2030,2031],{"class":232},", credential: ",[222,2033,2028],{"class":298},[222,2035,2036],{"class":232},", method: ",[222,2038,2028],{"class":298},[222,2040,2041],{"class":232},", host: ",[222,2043,2028],{"class":298},[222,2045,679],{"class":232},[222,2047,2048,2051,2053,2056,2058,2060,2062,2065,2068,2071,2073,2075,2078,2081],{"class":224,"line":270},[222,2049,2050],{"class":232},"                path: ",[222,2052,2028],{"class":298},[222,2054,2055],{"class":232},", body: ",[222,2057,284],{"class":298},[222,2059,455],{"class":228},[222,2061,331],{"class":228},[222,2063,2064],{"class":239},"\"\"",[222,2066,2067],{"class":232},", ttl: ",[222,2069,2070],{"class":298},"int",[222,2072,455],{"class":228},[222,2074,744],{"class":298},[222,2076,2077],{"class":232},") -> ",[222,2079,2080],{"class":298},"dict",[222,2082,2083],{"class":232},":\n",[222,2085,2086,2089,2091,2094,2097,2099],{"class":224,"line":305},[222,2087,2088],{"class":232},"    valid_until ",[222,2090,118],{"class":228},[222,2092,2093],{"class":298}," int",[222,2095,2096],{"class":232},"(time.time()) ",[222,2098,132],{"class":228},[222,2100,2101],{"class":232}," ttl\n",[222,2103,2104],{"class":224,"line":319},[222,2105,260],{"emptyLinePlaceholder":259},[222,2107,2108,2111,2113,2116,2118,2121,2124,2126,2129,2132,2135,2137,2139],{"class":224,"line":352},[222,2109,2110],{"class":232},"    priv ",[222,2112,118],{"class":228},[222,2114,2115],{"class":232}," base64.urlsafe_b64decode(app_key ",[222,2117,132],{"class":228},[222,2119,2120],{"class":239}," \"=\"",[222,2122,2123],{"class":228}," *",[222,2125,325],{"class":232},[222,2127,2128],{"class":228},"-",[222,2130,2131],{"class":298},"len",[222,2133,2134],{"class":232},"(app_key) ",[222,2136,540],{"class":228},[222,2138,534],{"class":298},[222,2140,585],{"class":232},[222,2142,2143,2146,2148,2151,2153],{"class":224,"line":410},[222,2144,2145],{"class":232},"    seed ",[222,2147,118],{"class":228},[222,2149,2150],{"class":232}," priv[:",[222,2152,1087],{"class":298},[222,2154,2155],{"class":232},"]\n",[222,2157,2158],{"class":224,"line":416},[222,2159,260],{"emptyLinePlaceholder":259},[222,2161,2162,2165,2167,2170,2173,2175,2177],{"class":224,"line":421},[222,2163,2164],{"class":232},"    h ",[222,2166,118],{"class":228},[222,2168,2169],{"class":232}," hashlib.blake2b(",[222,2171,2172],{"class":283},"digest_size",[222,2174,118],{"class":228},[222,2176,1087],{"class":298},[222,2178,407],{"class":232},[222,2180,2181],{"class":224,"line":446},[222,2182,2183],{"class":232},"    h.update(method.encode())\n",[222,2185,2186],{"class":224,"line":493},[222,2187,2188],{"class":232},"    h.update(host.encode())\n",[222,2190,2191],{"class":224,"line":547},[222,2192,2193],{"class":232},"    h.update(path.encode())\n",[222,2195,2196,2199,2202,2205],{"class":224,"line":588},[222,2197,2198],{"class":232},"    h.update(struct.pack(",[222,2200,2201],{"class":239},"\"\u003CQ\"",[222,2203,2204],{"class":232},", valid_until))   ",[222,2206,2207],{"class":266},"# little-endian uint64\n",[222,2209,2210,2212],{"class":224,"line":593},[222,2211,1629],{"class":228},[222,2213,2214],{"class":232}," body:\n",[222,2216,2217],{"class":224,"line":598},[222,2218,2219],{"class":232},"        h.update(body)\n",[222,2221,2222],{"class":224,"line":612},[222,2223,260],{"emptyLinePlaceholder":259},[222,2225,2226,2228,2230],{"class":224,"line":618},[222,2227,1883],{"class":232},[222,2229,118],{"class":228},[222,2231,2232],{"class":232}," SigningKey(seed).sign(h.digest()).signature\n",[222,2234,2235],{"class":224,"line":629},[222,2236,260],{"emptyLinePlaceholder":259},[222,2238,2239,2241],{"class":224,"line":635},[222,2240,1908],{"class":228},[222,2242,302],{"class":232},[222,2244,2245,2247],{"class":224,"line":645},[222,2246,1923],{"class":239},[222,2248,2249],{"class":232},": credential,\n",[222,2251,2252,2254,2256,2258],{"class":224,"line":650},[222,2253,1931],{"class":239},[222,2255,1384],{"class":232},[222,2257,2028],{"class":298},[222,2259,2260],{"class":232},"(valid_until),\n",[222,2262,2263,2265],{"class":224,"line":655},[222,2264,1950],{"class":239},[222,2266,2267],{"class":232},": base64.urlsafe_b64encode(sig).decode(),\n",[222,2269,2270],{"class":224,"line":669},[222,2271,1655],{"class":232},[15,2273,2275],{"id":2274},"choosing-an-expiry","Choosing an expiry",[11,2277,2278,2280],{},[52,2279,72],{}," is entirely yours to set, and it is the only replay protection in the scheme — there is\nno nonce and no server-side record of used signatures. A signed request can be replayed by anyone\nwho observes it until it expires.",[2282,2283,2284,2290,2300,2306],"ul",{},[165,2285,2286,2289],{},[23,2287,2288],{},"60 seconds"," is a sensible default for interactive calls.",[165,2291,2292,2295,2296,2299],{},[23,2293,2294],{},"Do not sign far into the future for convenience."," A one-year signature on a ",[52,2297,2298],{},"DELETE"," is a\none-year deletion capability sitting in whatever logged the URL.",[165,2301,2302,2305],{},[23,2303,2304],{},"Sign per request."," Because the path and body are covered, a signature is not reusable for a\ndifferent call anyway.",[165,2307,2308,2311,2312,2315,2316,2319],{},[23,2309,2310],{},"Query parameters are not covered",", so a signature for ",[52,2313,2314],{},"GET \u002Fobjects?limit=50"," is equally valid\nfor ",[52,2317,2318],{},"GET \u002Fobjects?limit=1000",". Treat parameters as unauthenticated.",[15,2321,2323],{"id":2322},"verifying-a-credential","Verifying a credential",[94,2325,2329],{"className":2326,"code":2327,"language":2328,"meta":102,"style":102},"language-bash shiki shiki-themes github-dark-default github-dark-default","curl -sD - -o \u002Fdev\u002Fnull \"$TESSERA_API\u002Fauth\u002Fcheck?$AUTH\"\n# HTTP\u002F1.1 204 No Content   → valid and active\n# HTTP\u002F1.1 401 Unauthorized → expired, unknown, revoked, or mis-signed\n","bash",[52,2330,2331,2361,2366],{"__ignoreMap":102},[222,2332,2333,2336,2339,2341,2344,2347,2350,2353,2356,2359],{"class":224,"line":225},[222,2334,2335],{"class":283},"curl",[222,2337,2338],{"class":298}," -sD",[222,2340,522],{"class":239},[222,2342,2343],{"class":298}," -o",[222,2345,2346],{"class":239}," \u002Fdev\u002Fnull",[222,2348,2349],{"class":239}," \"",[222,2351,2352],{"class":232},"$TESSERA_API",[222,2354,2355],{"class":239},"\u002Fauth\u002Fcheck?",[222,2357,2358],{"class":232},"$AUTH",[222,2360,1461],{"class":239},[222,2362,2363],{"class":224,"line":243},[222,2364,2365],{"class":266},"# HTTP\u002F1.1 204 No Content   → valid and active\n",[222,2367,2368],{"class":224,"line":256},[222,2369,2370],{"class":266},"# HTTP\u002F1.1 401 Unauthorized → expired, unknown, revoked, or mis-signed\n",[11,2372,2373,2376,2377,2380],{},[52,2374,2375],{},"204"," means the credential exists, is in the ",[52,2378,2379],{},"active"," state, and your signature verified.",[15,2382,2384],{"id":2383},"failure-modes","Failure modes",[11,2386,2387,2388,2391],{},"Every one of these returns ",[52,2389,2390],{},"401"," with a plain-text body. The body tells you which:",[28,2393,2394,2404],{},[31,2395,2396],{},[34,2397,2398,2401],{},[37,2399,2400],{},"Body",[37,2402,2403],{},"Cause",[44,2405,2406,2416,2426,2438,2450,2460,2471],{},[34,2407,2408,2413],{},[49,2409,2410],{},[52,2411,2412],{},"missing auth params",[49,2414,2415],{},"One of the three query parameters is absent.",[34,2417,2418,2423],{},[49,2419,2420],{},[52,2421,2422],{},"invalid validUntil",[49,2424,2425],{},"Not parseable as a base-10 integer.",[34,2427,2428,2433],{},[49,2429,2430],{},[52,2431,2432],{},"signature expired",[49,2434,2435,2437],{},[52,2436,72],{}," is in the past. Check clock skew before blaming us.",[34,2439,2440,2445],{},[49,2441,2442],{},[52,2443,2444],{},"invalid credential",[49,2446,2447,2449],{},[52,2448,54],{}," is not valid padded base64url.",[34,2451,2452,2457],{},[49,2453,2454],{},[52,2455,2456],{},"unknown credential",[49,2458,2459],{},"Decoded fine, but is not a provisioned, active key — including revoked keys.",[34,2461,2462,2467],{},[49,2463,2464],{},[52,2465,2466],{},"invalid signature",[49,2468,2469,2449],{},[52,2470,86],{},[34,2472,2473,2478],{},[49,2474,2475],{},[52,2476,2477],{},"signature mismatch",[49,2479,2480,2481,2483,2484,2486],{},"Verification failed. Usually a wrong ",[52,2482,200],{},", a hashed query string, big-endian ",[52,2485,72],{},", or SHA-256 instead of blake2b.",[104,2488,2490],{"title":2489,"tone":1404},"Debugging `signature mismatch`",[11,2491,2492,2493,2495,2496,2498,2499,2501],{},"Work through it in this order: is ",[52,2494,200],{}," exactly the ",[52,2497,204],{}," header, including port? Is the path free\nof the query string? Is ",[52,2500,72],{}," little-endian in the hash and decimal in the URL? Is the digest\nblake2b-256? Is the body byte-identical to what you transmitted — same serialisation, no\nre-encoding by an HTTP client in between? That last one catches more people than the rest combined.",[15,2503,2505],{"id":2504},"storing-credentials","Storing credentials",[11,2507,2508,2509,2511,2512,2515],{},"The ",[52,2510,1545],{}," returned by provisioning is the ",[23,2513,2514],{},"private key, shown exactly once",". It is not\nrecoverable and we do not have a copy.",[2282,2517,2518,2521,2524],{},[165,2519,2520],{},"Store it in a secret manager, not in configuration or version control.",[165,2522,2523],{},"It is the account, so rotating it means provisioning a new credential and re-registering objects\nunder it. There is no way to move existing objects between credentials.",[165,2525,2526,2527,2530],{},"Revoke compromised credentials immediately with ",[52,2528,2529],{},"DELETE \u002Fauth\u002Frevoke",". Revocation takes effect on\nthe next request; there is no token lifetime to wait out.",[2532,2533,2534],"style",{},"html pre.shiki code .srH4v, html code.shiki .srH4v{--shiki-default:#FF7B72;--shiki-dark:#FF7B72}html pre.shiki code .sb7RM, html code.shiki .sb7RM{--shiki-default:#E6EDF3;--shiki-dark:#E6EDF3}html pre.shiki code .skZ57, html code.shiki .skZ57{--shiki-default:#A5D6FF;--shiki-dark:#A5D6FF}html pre.shiki code .sI3U9, html code.shiki .sI3U9{--shiki-default:#8B949E;--shiki-dark:#8B949E}html pre.shiki code .sYGIp, html code.shiki .sYGIp{--shiki-default:#D2A8FF;--shiki-dark:#D2A8FF}html pre.shiki code .sAxfE, html code.shiki .sAxfE{--shiki-default:#FFA657;--shiki-dark:#FFA657}html pre.shiki code .sEQcL, html code.shiki .sEQcL{--shiki-default:#79C0FF;--shiki-dark:#79C0FF}html pre.shiki code .sq3Sb, html code.shiki .sq3Sb{--shiki-default:#7EE787;--shiki-default-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":102,"searchDepth":256,"depth":256,"links":2536},[2537,2538,2539,2540,2541,2542,2543,2544,2545],{"id":17,"depth":243,"text":18},{"id":136,"depth":243,"text":137},{"id":212,"depth":243,"text":213},{"id":1424,"depth":243,"text":1425},{"id":1984,"depth":243,"text":1985},{"id":2274,"depth":243,"text":2275},{"id":2322,"depth":243,"text":2323},{"id":2383,"depth":243,"text":2384},{"id":2504,"depth":243,"text":2505},"Every request is signed with ed25519 over a blake2b hash of the method, host, path, expiry, and body. Here is the exact construction, with working code.","md",{},"\u002Fdocs\u002Fauthentication","Guides",{"title":5,"description":2546},"docs\u002Fauthentication","HsQUxwOQ6qsmLn7DHdG06ApS2DYgHeHWPRYigB6R-_0",[2555,2560,2564,2568,2572,2576,2577,2582,2587,2591,2595,2599],{"path":2556,"title":2557,"description":2558,"section":2559,"order":270,"badge":6},"\u002Fdocs\u002Fapi\u002Faccount","Account","GET \u002Faccount — stored, read, and written byte counters for your credential.","API reference",{"path":2561,"title":2562,"description":2563,"section":2559,"order":263,"badge":6},"\u002Fdocs\u002Fapi\u002Fobjects","Objects","Register, read, list, and delete objects — ordered lists of slab segments with encrypted keys and opaque metadata.",{"path":2565,"title":2566,"description":2567,"section":2559,"order":243,"badge":6},"\u002Fdocs\u002Fapi\u002Fplacement","Placement","POST \u002Fprepare-write — get the providers, contracts, and access tokens for writing a slab's shards.",{"path":2569,"title":2570,"description":2571,"section":2559,"order":225,"badge":6},"\u002Fdocs\u002Fapi\u002Fprovisioning","Credentials","Provision, verify, and revoke the ed25519 credential that is both your API identity and your account.",{"path":2573,"title":2574,"description":2575,"section":2559,"order":256,"badge":6},"\u002Fdocs\u002Fapi\u002Fslabs","Slabs","Register, read, list, unpin, and prune erasure-coded slabs — the layer that records which provider holds which shard.",{"path":2549,"title":5,"description":2546,"section":2550,"order":243,"badge":6},{"path":2578,"title":2579,"description":2580,"section":2581,"order":243,"badge":6},"\u002Fdocs\u002Fconcepts","Concepts & data model","Sectors, shards, slabs, objects, and accounts — what each one is, why the model has no filenames, and what that means for your integration.","Introduction",{"path":2583,"title":2584,"description":2585,"section":2586,"order":225,"badge":6},"\u002Fdocs\u002Ferrors","Errors","Status codes, the two different error body formats, and the one case where a failure arrives with a 200.","Reference",{"path":2588,"title":2589,"description":2590,"section":2581,"order":225,"badge":6},"\u002Fdocs","Overview","Tessera is an HTTP API for storing encrypted, erasure-coded objects across independent storage providers. This is the reference for it.",{"path":2592,"title":2593,"description":2594,"section":2586,"order":243,"badge":6},"\u002Fdocs\u002Flimits","Limits & quotas","Rate limits, storage caps, geometry constraints, and the size and encoding limits that apply to every request.",{"path":2596,"title":2597,"description":2598,"section":2550,"order":225,"badge":6},"\u002Fdocs\u002Fquickstart","Quickstart","Provision a credential, upload an object across 15 providers, and read it back. Fifteen minutes, one file of code.",{"path":2600,"title":2601,"description":2602,"section":2550,"order":256,"badge":6},"\u002Fdocs\u002Frecovery","Recovery without Tessera","How to retrieve your data if Tessera is unavailable, unwilling, or permanently gone — and what you need to keep on hand for that to work.",1786190913116]