{"version":3,"sources":["../../src/protocol/write-request.ts"],"sourcesContent":["/**\n * Transport shared by every builder call that authenticates with the\n * Personal Server Write API: the data writes of\n * {@link ../protocol/personal-server-write} and the derivative question\n * routes of {@link ../protocol/derivative-questions}.\n *\n * @remarks\n * Both sign a single-use Web3Signed proof per request, so both need the same\n * two things: a `fetch` wrapper that re-signs on every transport attempt, and\n * one process-wide record of the `iat` seconds already issued, so two proofs\n * for the same request identity can never come out byte-identical (the server\n * would reject the second as a replay). The record must be shared, not\n * per-module: a builder that polls one question every few milliseconds signs\n * the same `{ aud, method, uri, bodyHash, grantId }` many times a second.\n *\n * @internal\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { WriteRequestError, WriteTransportError } from \"../errors\";\n\n/**\n * Transport-level retry knobs shared by every Write API call.\n *\n * @remarks\n * Applies only when `fetch` **throws** (connection reset, DNS, a relay drop).\n * Every attempt signs a fresh proof, because the Personal Server consumes a\n * proof the moment it accepts it. A received HTTP response is never retried:\n * a 4xx/5xx is surfaced as a typed error.\n * @category Protocol\n */\nexport interface WriteTransportRetryOptions {\n  /** Total attempts including the first (default 3). `1` disables retries. */\n  attempts?: number;\n  /** Delay before the first retry (ms); doubles per retry (default 1_000). */\n  initialDelayMs?: number;\n}\n\n/** Strip trailing slashes so a base URL concatenates with a path. */\nexport function normalizeBaseUrl(url: string): string {\n  return url.replace(/\\/+$/, \"\");\n}\n\n/** The caller's `fetch`, else the global one. */\nexport function resolveFetch(fetchFn: typeof fetch | undefined): typeof fetch {\n  const resolved = fetchFn ?? globalThis.fetch;\n  if (resolved === undefined) {\n    throw new WriteRequestError(\"No fetch implementation available\");\n  }\n  return resolved;\n}\n\nexport function errorMessage(err: unknown): string {\n  return err instanceof Error ? err.message : String(err);\n}\n\nexport function finiteOr(value: number | undefined, fallback: number): number {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nexport function sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The Personal Server consumes every proof it accepts, and a Web3Signed\n * payload is fully determined by `{ aud, method, uri, bodyHash, grantId, iat,\n * exp }`, so two proofs for the same request signed within one second would\n * be byte-identical and the second rejected as a replay. Remember the\n * highest `iat` issued per request identity in this process and bump past\n * it when a second proof for the same identity falls inside the same second.\n *\n * A mark is kept for as long as the server can still remember the proof it\n * guards (its lifetime plus the verifier's clock skew), so a proof is never\n * re-issued while it could still be rejected as a replay: not by a burst,\n * and not by a wall clock stepping backwards (an identical request after a\n * step back waits for the clock instead of reusing the mark). Marks are\n * bucketed by their `iat` second, so pruning (once per second) only touches\n * the buckets that fell out of the retention window: the work per proof is\n * amortised constant and the map is bounded by the distinct requests signed\n * in the window.\n */\nconst issuedProofIats = new Map<string, number>();\n/** `iat` second -> identities whose current mark is that second. */\nconst issuedProofBuckets = new Map<number, Set<string>>();\nlet issuedProofIatsPrunedAtSec = 0;\n/** `buildWeb3SignedHeader`'s default `exp - iat`. */\nconst WEB3_SIGNED_PROOF_LIFETIME_SECONDS = 300;\n/** The verifier's tolerated clock skew (`verifyWeb3Signed`). */\nconst WEB3_SIGNED_CLOCK_SKEW_SECONDS = 60;\nconst PROOF_IAT_RETENTION_SECONDS =\n  WEB3_SIGNED_PROOF_LIFETIME_SECONDS + WEB3_SIGNED_CLOCK_SKEW_SECONDS;\n/**\n * How far ahead of the clock a bumped `iat` may run when the proof is sent.\n * The verifier tolerates 60 s of skew. A burst of identical requests that\n * would need to run further ahead waits for the clock instead, so a proof is\n * never repeated; sustained identical requests are throttled to one per\n * second after a burst of this many.\n */\nconst PROOF_IAT_MAX_AHEAD_SECONDS = 30;\n\nfunction pruneIssuedProofIats(nowSec: number): void {\n  if (issuedProofIatsPrunedAtSec === nowSec) return;\n  issuedProofIatsPrunedAtSec = nowSec;\n  const cutoff = nowSec - PROOF_IAT_RETENTION_SECONDS;\n  // There is at most one bucket per second in the window, so this walk is\n  // bounded by the window length, not by the number of marks.\n  for (const [sec, keys] of issuedProofBuckets) {\n    if (sec >= cutoff) continue;\n    for (const key of keys) issuedProofIats.delete(key);\n    issuedProofBuckets.delete(sec);\n  }\n}\n\nfunction setIssuedProofIat(key: string, iat: number, previous?: number): void {\n  if (previous !== undefined) {\n    const bucket = issuedProofBuckets.get(previous);\n    bucket?.delete(key);\n    if (bucket?.size === 0) issuedProofBuckets.delete(previous);\n  }\n  issuedProofIats.set(key, iat);\n  let bucket = issuedProofBuckets.get(iat);\n  if (bucket === undefined) {\n    bucket = new Set();\n    issuedProofBuckets.set(iat, bucket);\n  }\n  bucket.add(key);\n}\n\n/**\n * Reserve the next `iat` for a request identity. The reservation is made\n * synchronously so concurrent callers never share a value; the returned\n * promise only waits when the reserved `iat` is further ahead of the clock\n * than {@link PROOF_IAT_MAX_AHEAD_SECONDS}.\n */\nexport function nextProofIat(proofKey: string): Promise<number> {\n  const nowSec = Math.floor(Date.now() / 1000);\n  pruneIssuedProofIats(nowSec);\n  const last = issuedProofIats.get(proofKey);\n  const iat = last === undefined ? nowSec : Math.max(nowSec, last + 1);\n  setIssuedProofIat(proofKey, iat, last);\n  const waitSec = iat - nowSec - PROOF_IAT_MAX_AHEAD_SECONDS;\n  if (waitSec <= 0) return Promise.resolve(iat);\n  return sleep(waitSec * 1000).then(() => iat);\n}\n\n/**\n * A fresh `nonce` claim for one proof.\n *\n * @remarks\n * The Personal Server keys its replay guard on `(builder, nonce)` when a\n * proof carries a nonce, and on the whole proof when it does not. A nonce is\n * therefore what makes two identical requests signed inside the same second\n * distinct instead of the second being refused as a replay, which is the\n * difference between a poll loop that works and one that dies on its second\n * pass. Every question call sends one.\n */\nexport function freshProofNonce(): string {\n  const webCrypto = globalThis.crypto;\n  if (typeof webCrypto?.randomUUID === \"function\") {\n    return webCrypto.randomUUID();\n  }\n  // Older runtimes expose getRandomValues without randomUUID; 16 random bytes\n  // are the same uniqueness with a different spelling.\n  if (typeof webCrypto?.getRandomValues === \"function\") {\n    return bytesToHex(webCrypto.getRandomValues(new Uint8Array(16)));\n  }\n  throw new WriteRequestError(\n    \"No secure random source available to build a proof nonce; provide a crypto global\",\n  );\n}\n\n/** The identity a proof is deduplicated by. */\nexport function proofKeyFor(parts: {\n  aud: string;\n  method: string;\n  uri: string;\n  grantId: string;\n  signedBytes?: Uint8Array;\n}): string {\n  // A digest, so a retained mark costs a fixed amount of memory whatever the\n  // request looked like.\n  return bytesToHex(\n    sha256(\n      new TextEncoder().encode(\n        JSON.stringify([\n          parts.aud,\n          parts.method,\n          parts.uri,\n          parts.grantId,\n          parts.signedBytes ? bytesToHex(sha256(parts.signedBytes)) : \"\",\n        ]),\n      ),\n    ),\n  );\n}\n\n/**\n * Send a request, re-signing it on every attempt. Only a thrown `fetch` is\n * retried; the proof builder and any received response are never retried.\n */\nexport async function sendWithFreshProof(\n  label: string,\n  fetchFn: typeof fetch,\n  options: WriteTransportRetryOptions | undefined,\n  proofKey: string,\n  build: (iat: number) => Promise<{ url: string; init: RequestInit }>,\n): Promise<Response> {\n  const attempts = Math.max(1, Math.floor(finiteOr(options?.attempts, 3)));\n  let delayMs = Math.max(0, finiteOr(options?.initialDelayMs, 1_000));\n  let lastError: unknown;\n  for (let attempt = 0; attempt < attempts; attempt++) {\n    const { url, init } = await build(await nextProofIat(proofKey));\n    try {\n      return await fetchFn(url, init);\n    } catch (err) {\n      lastError = err;\n    }\n    if (attempt < attempts - 1) {\n      await sleep(delayMs);\n      delayMs *= 2;\n    }\n  }\n  throw new WriteTransportError(\n    `${label} failed after ${attempts} attempt(s): ${errorMessage(lastError)}`,\n    attempts,\n    lastError,\n  );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,kBAAuB;AACvB,kBAA2B;AAC3B,oBAAuD;AAoBhD,SAAS,iBAAiB,KAAqB;AACpD,SAAO,IAAI,QAAQ,QAAQ,EAAE;AAC/B;AAGO,SAAS,aAAa,SAAiD;AAC5E,QAAM,WAAW,WAAW,WAAW;AACvC,MAAI,aAAa,QAAW;AAC1B,UAAM,IAAI,gCAAkB,mCAAmC;AAAA,EACjE;AACA,SAAO;AACT;AAEO,SAAS,aAAa,KAAsB;AACjD,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEO,SAAS,SAAS,OAA2B,UAA0B;AAC5E,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAEO,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAoBA,MAAM,kBAAkB,oBAAI,IAAoB;AAEhD,MAAM,qBAAqB,oBAAI,IAAyB;AACxD,IAAI,6BAA6B;AAEjC,MAAM,qCAAqC;AAE3C,MAAM,iCAAiC;AACvC,MAAM,8BACJ,qCAAqC;AAQvC,MAAM,8BAA8B;AAEpC,SAAS,qBAAqB,QAAsB;AAClD,MAAI,+BAA+B,OAAQ;AAC3C,+BAA6B;AAC7B,QAAM,SAAS,SAAS;AAGxB,aAAW,CAAC,KAAK,IAAI,KAAK,oBAAoB;AAC5C,QAAI,OAAO,OAAQ;AACnB,eAAW,OAAO,KAAM,iBAAgB,OAAO,GAAG;AAClD,uBAAmB,OAAO,GAAG;AAAA,EAC/B;AACF;AAEA,SAAS,kBAAkB,KAAa,KAAa,UAAyB;AAC5E,MAAI,aAAa,QAAW;AAC1B,UAAMA,UAAS,mBAAmB,IAAI,QAAQ;AAC9C,IAAAA,SAAQ,OAAO,GAAG;AAClB,QAAIA,SAAQ,SAAS,EAAG,oBAAmB,OAAO,QAAQ;AAAA,EAC5D;AACA,kBAAgB,IAAI,KAAK,GAAG;AAC5B,MAAI,SAAS,mBAAmB,IAAI,GAAG;AACvC,MAAI,WAAW,QAAW;AACxB,aAAS,oBAAI,IAAI;AACjB,uBAAmB,IAAI,KAAK,MAAM;AAAA,EACpC;AACA,SAAO,IAAI,GAAG;AAChB;AAQO,SAAS,aAAa,UAAmC;AAC9D,QAAM,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC3C,uBAAqB,MAAM;AAC3B,QAAM,OAAO,gBAAgB,IAAI,QAAQ;AACzC,QAAM,MAAM,SAAS,SAAY,SAAS,KAAK,IAAI,QAAQ,OAAO,CAAC;AACnE,oBAAkB,UAAU,KAAK,IAAI;AACrC,QAAM,UAAU,MAAM,SAAS;AAC/B,MAAI,WAAW,EAAG,QAAO,QAAQ,QAAQ,GAAG;AAC5C,SAAO,MAAM,UAAU,GAAI,EAAE,KAAK,MAAM,GAAG;AAC7C;AAaO,SAAS,kBAA0B;AACxC,QAAM,YAAY,WAAW;AAC7B,MAAI,OAAO,WAAW,eAAe,YAAY;AAC/C,WAAO,UAAU,WAAW;AAAA,EAC9B;AAGA,MAAI,OAAO,WAAW,oBAAoB,YAAY;AACpD,eAAO,wBAAW,UAAU,gBAAgB,IAAI,WAAW,EAAE,CAAC,CAAC;AAAA,EACjE;AACA,QAAM,IAAI;AAAA,IACR;AAAA,EACF;AACF;AAGO,SAAS,YAAY,OAMjB;AAGT,aAAO;AAAA,QACL;AAAA,MACE,IAAI,YAAY,EAAE;AAAA,QAChB,KAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM,kBAAc,4BAAW,oBAAO,MAAM,WAAW,CAAC,IAAI;AAAA,QAC9D,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAsB,mBACpB,OACA,SACA,SACA,UACA,OACmB;AACnB,QAAM,WAAW,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,SAAS,UAAU,CAAC,CAAC,CAAC;AACvE,MAAI,UAAU,KAAK,IAAI,GAAG,SAAS,SAAS,gBAAgB,GAAK,CAAC;AAClE,MAAI;AACJ,WAAS,UAAU,GAAG,UAAU,UAAU,WAAW;AACnD,UAAM,EAAE,KAAK,KAAK,IAAI,MAAM,MAAM,MAAM,aAAa,QAAQ,CAAC;AAC9D,QAAI;AACF,aAAO,MAAM,QAAQ,KAAK,IAAI;AAAA,IAChC,SAAS,KAAK;AACZ,kBAAY;AAAA,IACd;AACA,QAAI,UAAU,WAAW,GAAG;AAC1B,YAAM,MAAM,OAAO;AACnB,iBAAW;AAAA,IACb;AAAA,EACF;AACA,QAAM,IAAI;AAAA,IACR,GAAG,KAAK,iBAAiB,QAAQ,gBAAgB,aAAa,SAAS,CAAC;AAAA,IACxE;AAAA,IACA;AAAA,EACF;AACF;","names":["bucket"]}