@opendatalabs/vana-sdk 4.3.1 → 4.4.0-connect-sheet.0-pr.218.295a8da

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +131 -0
  2. package/dist/auth/web3-signed-builder.cjs.map +1 -1
  3. package/dist/auth/web3-signed-builder.d.ts +12 -6
  4. package/dist/auth/web3-signed-builder.js.map +1 -1
  5. package/dist/auth/web3-signed.cjs +11 -0
  6. package/dist/auth/web3-signed.cjs.map +1 -1
  7. package/dist/auth/web3-signed.d.ts +7 -0
  8. package/dist/auth/web3-signed.js +10 -0
  9. package/dist/auth/web3-signed.js.map +1 -1
  10. package/dist/connect/account-origin.cjs +72 -0
  11. package/dist/connect/account-origin.cjs.map +1 -0
  12. package/dist/connect/account-origin.d.ts +92 -0
  13. package/dist/connect/account-origin.js +47 -0
  14. package/dist/connect/account-origin.js.map +1 -0
  15. package/dist/connect/account-origin.test.d.ts +1 -0
  16. package/dist/connect/chrome.cjs +221 -0
  17. package/dist/connect/chrome.cjs.map +1 -0
  18. package/dist/connect/chrome.d.ts +40 -0
  19. package/dist/connect/chrome.js +193 -0
  20. package/dist/connect/chrome.js.map +1 -0
  21. package/dist/connect/http-transports.cjs +100 -0
  22. package/dist/connect/http-transports.cjs.map +1 -0
  23. package/dist/connect/http-transports.d.ts +77 -0
  24. package/dist/connect/http-transports.js +75 -0
  25. package/dist/connect/http-transports.js.map +1 -0
  26. package/dist/connect/http-transports.test.d.ts +1 -0
  27. package/dist/connect/launcher.cjs +224 -0
  28. package/dist/connect/launcher.cjs.map +1 -0
  29. package/dist/connect/launcher.d.ts +122 -0
  30. package/dist/connect/launcher.js +210 -0
  31. package/dist/connect/launcher.js.map +1 -0
  32. package/dist/connect/launcher.test.d.ts +1 -0
  33. package/dist/connect/protocol.cjs +244 -0
  34. package/dist/connect/protocol.cjs.map +1 -0
  35. package/dist/connect/protocol.d.ts +307 -0
  36. package/dist/connect/protocol.js +203 -0
  37. package/dist/connect/protocol.js.map +1 -0
  38. package/dist/connect/protocol.test.d.ts +1 -0
  39. package/dist/connect.cjs +55 -0
  40. package/dist/connect.cjs.map +1 -0
  41. package/dist/connect.d.ts +35 -0
  42. package/dist/connect.js +34 -0
  43. package/dist/connect.js.map +1 -0
  44. package/dist/direct/connect-flow-sheet.test.d.ts +1 -0
  45. package/dist/direct/connect-flow.cjs +211 -1
  46. package/dist/direct/connect-flow.cjs.map +1 -1
  47. package/dist/direct/connect-flow.d.ts +75 -0
  48. package/dist/direct/connect-flow.js +213 -1
  49. package/dist/direct/connect-flow.js.map +1 -1
  50. package/dist/direct/connect-handler.cjs +493 -0
  51. package/dist/direct/connect-handler.cjs.map +1 -0
  52. package/dist/direct/connect-handler.d.ts +241 -0
  53. package/dist/direct/connect-handler.js +476 -0
  54. package/dist/direct/connect-handler.js.map +1 -0
  55. package/dist/direct/connect-handler.test.d.ts +1 -0
  56. package/dist/direct/controller.cjs +58 -3
  57. package/dist/direct/controller.cjs.map +1 -1
  58. package/dist/direct/controller.d.ts +79 -3
  59. package/dist/direct/controller.js +54 -2
  60. package/dist/direct/controller.js.map +1 -1
  61. package/dist/direct/endpoints.cjs +5 -2
  62. package/dist/direct/endpoints.cjs.map +1 -1
  63. package/dist/direct/endpoints.js +5 -2
  64. package/dist/direct/endpoints.js.map +1 -1
  65. package/dist/direct/request-authorization.cjs +65 -0
  66. package/dist/direct/request-authorization.cjs.map +1 -0
  67. package/dist/direct/request-authorization.d.ts +65 -0
  68. package/dist/direct/request-authorization.js +39 -0
  69. package/dist/direct/request-authorization.js.map +1 -0
  70. package/dist/direct/request-authorization.test.d.ts +1 -0
  71. package/dist/direct/types.cjs.map +1 -1
  72. package/dist/direct/types.d.ts +6 -0
  73. package/dist/direct/types.js.map +1 -1
  74. package/dist/direct/use-direct-vana-connect.cjs +18 -1
  75. package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
  76. package/dist/direct/use-direct-vana-connect.js +18 -1
  77. package/dist/direct/use-direct-vana-connect.js.map +1 -1
  78. package/dist/direct/use-vana-connect.cjs +84 -0
  79. package/dist/direct/use-vana-connect.cjs.map +1 -0
  80. package/dist/direct/use-vana-connect.d.ts +59 -0
  81. package/dist/direct/use-vana-connect.js +64 -0
  82. package/dist/direct/use-vana-connect.js.map +1 -0
  83. package/dist/direct/use-vana-connect.test.d.ts +1 -0
  84. package/dist/index.browser.js +9 -0
  85. package/dist/index.browser.js.map +2 -2
  86. package/dist/index.node.cjs +9 -0
  87. package/dist/index.node.cjs.map +2 -2
  88. package/dist/index.node.js +9 -0
  89. package/dist/index.node.js.map +2 -2
  90. package/dist/react.cjs +10 -2
  91. package/dist/react.cjs.map +1 -1
  92. package/dist/react.d.ts +4 -1
  93. package/dist/react.js +11 -1
  94. package/dist/react.js.map +1 -1
  95. package/dist/server.cjs +44 -2
  96. package/dist/server.cjs.map +1 -1
  97. package/dist/server.d.ts +6 -2
  98. package/dist/server.js +49 -3
  99. package/dist/server.js.map +1 -1
  100. package/package.json +11 -1
package/README.md CHANGED
@@ -457,6 +457,137 @@ pass the same explicit `idempotencyKey` on the retry to avoid a duplicate DCR.
457
457
  `react` is an optional peer dependency. The underlying
458
458
  `createDirectConnectFlow` store is also exported for non-React frontends.
459
459
 
460
+ ### Connect in Vana's in-page sheet
461
+
462
+ Instead of a Vana tab, the person can approve in a sheet Vana draws over your
463
+ page (it signs them in, shows the request, and finishes inside the sheet). The
464
+ sheet is `presentation: "sheet"` of the same flow: the request, the status poll
465
+ and the read are unchanged. The integration is two files.
466
+
467
+ Backend: one catch-all route serves `config`, `request`, `status` and `data`,
468
+ binding each request to the browser that created it.
469
+
470
+ ```ts
471
+ // app/api/vana/[action]/route.ts
472
+ import { createVanaConnectHandler } from "@opendatalabs/vana-sdk/server";
473
+ import { vana } from "@/lib/vana"; // your createDirectDataController(...)
474
+
475
+ const handler = createVanaConnectHandler({
476
+ controller: vana,
477
+ returnUrl: `${process.env.APP_URL}/connect/return`,
478
+ clientId: "my-app", // the embed client id Vana Account registered for your origin
479
+ // Recommended in production: only your signed-in users create requests.
480
+ session: async (request) => (await getSession(request))?.userId ?? null,
481
+ });
482
+ export { handler as GET, handler as POST };
483
+ ```
484
+
485
+ The handler reports the Vana Account origin from the controller's `env`
486
+ (`account.vana.org` for `production`, `account-dev.vana.org` for `dev`), so a
487
+ dev app never frames production.
488
+
489
+ Frontend:
490
+
491
+ ```tsx
492
+ "use client";
493
+ import { useVanaConnect } from "@opendatalabs/vana-sdk/react";
494
+
495
+ export function ConnectButton() {
496
+ const { state, start } = useVanaConnect<MyData>({ endpoint: "/api/vana" });
497
+ if (state.type === "done") return <Result data={state.result.data} />;
498
+ return (
499
+ <>
500
+ <button
501
+ disabled={
502
+ state.type === "creating" || state.type === "awaiting_approval"
503
+ }
504
+ onClick={start}
505
+ >
506
+ Connect with Vana
507
+ </button>
508
+ {state.type === "awaiting_approval" && state.popupBlocked && (
509
+ // The sheet could not run here (state.sheetUnavailable says why).
510
+ <a href={state.request.approvalUrl} target="_blank" rel="noreferrer">
511
+ Open Vana
512
+ </a>
513
+ )}
514
+ </>
515
+ );
516
+ }
517
+ ```
518
+
519
+ - While the sheet is up, `state.sheet` reports `completion` (`in_browser` or
520
+ `remote_collect`) once the person approves, coarse `progress`
521
+ (`collecting`, `writing`), and any `error` it is showing.
522
+ - **Approval is not delivery.** The sheet's `connected` event (and
523
+ `state.sheet.completion`) means the person approved; the data may still be
524
+ collecting. The flow reaches `done` only when your backend's status poll
525
+ says the request is readable and the read succeeds, and `error` when the
526
+ poll reports `denied`, `expired` or `completed` or the read fails. The sheet
527
+ sends no terminal event of its own; its events only make the flow poll
528
+ sooner.
529
+ - Closing the sheet before approving returns to `idle`. Closing it after
530
+ approving keeps polling, since the data may still be readable.
531
+ - If the frame cannot run (`frame_blocked`, `origin_not_registered`,
532
+ `incompatible_version`), the flow falls back to the tab presentation with
533
+ `popupBlocked: true`, so the "Open approval" link above still finishes it.
534
+ `origin_not_registered` arrives at once: the launcher asks Vana
535
+ (`GET <account>/api/embed/connect/client`, no credentials) alongside
536
+ framing. `vana_unavailable` means Vana's own side is down or misconfigured;
537
+ the sheet stays open and says so.
538
+ - The partner reads; it cannot write through the sheet. Pass
539
+ `sheet: { closeAfterDoneMs: null }` to leave the sheet open after the read.
540
+ - If your page sets a CSP, allow the frame and the client check:
541
+ `vanaConnectCsp(env)` from `/server` (or `/connect/protocol`) returns
542
+ `{ "frame-src": [accountOrigin], "connect-src": [accountOrigin] }`. It has
543
+ no default environment.
544
+ - `createVanaConnectHandler` takes `session` to bind requests to your own
545
+ signed-in user (recommended: without it anyone who loads your page can
546
+ create requests as your app), `createOptions` for per-request create input
547
+ (questions, owner), and `read` to replace the default `readApprovedData`.
548
+ Use `toNodeHandler(handler)` for Express or plain `http`.
549
+ - Creating a request is rate limited. The default, `createMemoryRateLimit()`,
550
+ allows 10 creates a minute per client IP and per session, in memory. It is
551
+ not production-grade (each instance counts alone, and the IP comes from
552
+ proxy headers): pass `rateLimit` backed by a shared store, or `false` when
553
+ a limiter already sits in front of the route. A refusal is
554
+ `429 rate_limited` with `Retry-After`.
555
+
556
+ Choosing the Vana Account origin yourself (a custom flow, or preview deploys
557
+ of Vana Account) goes through `resolveVanaAccountOrigin`; never match origins
558
+ with your own pattern. It returns the environment's origin unless overrides
559
+ are switched on for this build and the override is exactly an `https` origin
560
+ on your allowlist:
561
+
562
+ ```ts
563
+ import {
564
+ parseOriginAllowlist,
565
+ resolveVanaAccountOrigin,
566
+ } from "@opendatalabs/vana-sdk/connect";
567
+
568
+ const accountOrigin = resolveVanaAccountOrigin({
569
+ env: "dev",
570
+ override: new URLSearchParams(location.search).get("vana_account"),
571
+ previewOrigins: parseOriginAllowlist(
572
+ process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,
573
+ ),
574
+ // From build-time config, never the request. Off in production.
575
+ allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === "preview",
576
+ });
577
+ ```
578
+
579
+ `createDirectConnectFlow` and the launcher take that origin (or an `env`)
580
+ explicitly; neither falls back to production.
581
+
582
+ Lower-level pieces: `createDirectConnectFlow(transports, { presentation:
583
+ "sheet", sheet: { clientId, appName, accountOrigin } })` with an
584
+ `authorizeRequest` transport backed by
585
+ `controller.authorizeRequestRead(requestId)`; the launcher itself
586
+ (`@opendatalabs/vana-sdk/connect`); and the zero-dependency message protocol
587
+ (`@opendatalabs/vana-sdk/connect/protocol`). A backend in another language
588
+ implements the [connect wire contract](./src/connect/WIRE_CONTRACT.md), which has test
589
+ vectors.
590
+
460
591
  ### Test with large sample data
461
592
 
462
593
  When testing with realistic exports, use the public fixture catalog in
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /** Expiry (unix seconds). Defaults to `iat + 300`. */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /**\n * Optional uniqueness claim, attached as the `nonce` claim (a uuid is the\n * intended shape; the Personal Server bounds it at 128 characters).\n *\n * @remarks\n * A payload is otherwise fully determined by\n * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical\n * requests signed inside the same second produce the same bytes and the\n * Personal Server refuses the second as a replay. With a nonce the replay\n * key becomes `(signer, nonce)` instead, which is what lets a poll loop\n * send the same request twice. The nonce is then single use itself:\n * re-using one is a replay even when the rest of the payload changed.\n */\n nonce?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Merged before the sort, so the nonce sits in its alphabetical place like\n // every other claim and the payload stays deterministic.\n if (params.nonce !== undefined) {\n payload[\"nonce\"] = params.nonce;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,kBAAuB;AACvB,kBAA2B;AAC3B,sBAAyB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,aAAO,0BAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,aAAS,oBAAO,IAAI;AAC1B,SAAO,cAAU,wBAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,QAiCxB;AAClB,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,MAAM,OAAO,OAAO;AAC1B,QAAM,MAAM,OAAO,OAAO,MAAM;AAEhC,QAAM,UAAmC;AAAA,IACvC,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO,YAAY,gBAAgB,OAAO,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd;AAEA,MAAI,OAAO,YAAY,QAAW;AAChC,YAAQ,SAAS,IAAI,OAAO;AAAA,EAC9B;AAIA,MAAI,OAAO,UAAU,QAAW;AAC9B,YAAQ,OAAO,IAAI,OAAO;AAAA,EAC5B;AAGA,QAAM,gBAAgB,OAAO,KAAK,OAAO,EACtC,KAAK,EACL,OAAgC,CAAC,KAAK,QAAQ;AAC7C,QAAI,GAAG,IAAI,QAAQ,GAAG;AACtB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEP,QAAM,cAAc,KAAK,UAAU,aAAa;AAChD,QAAM,eAAe,IAAI,YAAY,EAAE,OAAO,WAAW;AACzD,QAAM,gBAAgB,gBAAgB,YAAY;AAElD,QAAM,YAAY,MAAM,OAAO,YAAY,aAAa;AAExD,SAAO,cAAc,aAAa,IAAI,SAAS;AACjD;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /**\n * Expiry (unix seconds). Defaults to `iat + 300`. Verifiers reject a window\n * longer than 15 minutes after `iat` (see `verifyWeb3Signed`).\n */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /**\n * Optional uniqueness claim, attached as the `nonce` claim (a uuid is the\n * intended shape; the Personal Server bounds it at 128 characters).\n *\n * @remarks\n * A payload is otherwise fully determined by\n * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical\n * requests signed inside the same second produce the same bytes. On the\n * routes that keep a replay store (the MCP and write-session handshakes\n * and delegated writes) the Personal Server refuses the second as a\n * replay; with a nonce the replay key becomes `(signer, nonce)` instead,\n * which is what lets a poll loop send the same request twice. The nonce\n * is then single use itself: re-using one is a replay even when the rest\n * of the payload changed. Plain data reads keep no replay store; their\n * exposure is bounded by the lifetime cap.\n */\n nonce?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Merged before the sort, so the nonce sits in its alphabetical place like\n // every other claim and the payload stays deterministic.\n if (params.nonce !== undefined) {\n payload[\"nonce\"] = params.nonce;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,kBAAuB;AACvB,kBAA2B;AAC3B,sBAAyB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,aAAO,0BAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,aAAS,oBAAO,IAAI;AAC1B,SAAO,cAAU,wBAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,QAuCxB;AAClB,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,MAAM,OAAO,OAAO;AAC1B,QAAM,MAAM,OAAO,OAAO,MAAM;AAEhC,QAAM,UAAmC;AAAA,IACvC,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO,YAAY,gBAAgB,OAAO,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd;AAEA,MAAI,OAAO,YAAY,QAAW;AAChC,YAAQ,SAAS,IAAI,OAAO;AAAA,EAC9B;AAIA,MAAI,OAAO,UAAU,QAAW;AAC9B,YAAQ,OAAO,IAAI,OAAO;AAAA,EAC5B;AAGA,QAAM,gBAAgB,OAAO,KAAK,OAAO,EACtC,KAAK,EACL,OAAgC,CAAC,KAAK,QAAQ;AAC7C,QAAI,GAAG,IAAI,QAAQ,GAAG;AACtB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEP,QAAM,cAAc,KAAK,UAAU,aAAa;AAChD,QAAM,eAAe,IAAI,YAAY,EAAE,OAAO,WAAW;AACzD,QAAM,gBAAgB,gBAAgB,YAAY;AAElD,QAAM,YAAY,MAAM,OAAO,YAAY,aAAa;AAExD,SAAO,cAAc,aAAa,IAAI,SAAS;AACjD;","names":[]}
@@ -38,7 +38,10 @@ export declare function buildWeb3SignedHeader(params: {
38
38
  body?: Uint8Array;
39
39
  /** Issued-at (unix seconds). Defaults to now. */
40
40
  iat?: number;
41
- /** Expiry (unix seconds). Defaults to `iat + 300`. */
41
+ /**
42
+ * Expiry (unix seconds). Defaults to `iat + 300`. Verifiers reject a window
43
+ * longer than 15 minutes after `iat` (see `verifyWeb3Signed`).
44
+ */
42
45
  exp?: number;
43
46
  /** Optional grant id, attached as the `grantId` claim. */
44
47
  grantId?: string;
@@ -49,11 +52,14 @@ export declare function buildWeb3SignedHeader(params: {
49
52
  * @remarks
50
53
  * A payload is otherwise fully determined by
51
54
  * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical
52
- * requests signed inside the same second produce the same bytes and the
53
- * Personal Server refuses the second as a replay. With a nonce the replay
54
- * key becomes `(signer, nonce)` instead, which is what lets a poll loop
55
- * send the same request twice. The nonce is then single use itself:
56
- * re-using one is a replay even when the rest of the payload changed.
55
+ * requests signed inside the same second produce the same bytes. On the
56
+ * routes that keep a replay store (the MCP and write-session handshakes
57
+ * and delegated writes) the Personal Server refuses the second as a
58
+ * replay; with a nonce the replay key becomes `(signer, nonce)` instead,
59
+ * which is what lets a poll loop send the same request twice. The nonce
60
+ * is then single use itself: re-using one is a replay even when the rest
61
+ * of the payload changed. Plain data reads keep no replay store; their
62
+ * exposure is bounded by the lifetime cap.
57
63
  */
58
64
  nonce?: string;
59
65
  /** Pre-computed `bodyHash` claim — overrides `body`. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /** Expiry (unix seconds). Defaults to `iat + 300`. */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /**\n * Optional uniqueness claim, attached as the `nonce` claim (a uuid is the\n * intended shape; the Personal Server bounds it at 128 characters).\n *\n * @remarks\n * A payload is otherwise fully determined by\n * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical\n * requests signed inside the same second produce the same bytes and the\n * Personal Server refuses the second as a replay. With a nonce the replay\n * key becomes `(signer, nonce)` instead, which is what lets a poll loop\n * send the same request twice. The nonce is then single use itself:\n * re-using one is a replay even when the rest of the payload changed.\n */\n nonce?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Merged before the sort, so the nonce sits in its alphabetical place like\n // every other claim and the payload stays deterministic.\n if (params.nonce !== undefined) {\n payload[\"nonce\"] = params.nonce;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":"AAgBA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,SAAO,SAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,IAAI;AAC1B,SAAO,UAAU,WAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,QAiCxB;AAClB,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,MAAM,OAAO,OAAO;AAC1B,QAAM,MAAM,OAAO,OAAO,MAAM;AAEhC,QAAM,UAAmC;AAAA,IACvC,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO,YAAY,gBAAgB,OAAO,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd;AAEA,MAAI,OAAO,YAAY,QAAW;AAChC,YAAQ,SAAS,IAAI,OAAO;AAAA,EAC9B;AAIA,MAAI,OAAO,UAAU,QAAW;AAC9B,YAAQ,OAAO,IAAI,OAAO;AAAA,EAC5B;AAGA,QAAM,gBAAgB,OAAO,KAAK,OAAO,EACtC,KAAK,EACL,OAAgC,CAAC,KAAK,QAAQ;AAC7C,QAAI,GAAG,IAAI,QAAQ,GAAG;AACtB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEP,QAAM,cAAc,KAAK,UAAU,aAAa;AAChD,QAAM,eAAe,IAAI,YAAY,EAAE,OAAO,WAAW;AACzD,QAAM,gBAAgB,gBAAgB,YAAY;AAElD,QAAM,YAAY,MAAM,OAAO,YAAY,aAAa;AAExD,SAAO,cAAc,aAAa,IAAI,SAAS;AACjD;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /**\n * Expiry (unix seconds). Defaults to `iat + 300`. Verifiers reject a window\n * longer than 15 minutes after `iat` (see `verifyWeb3Signed`).\n */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /**\n * Optional uniqueness claim, attached as the `nonce` claim (a uuid is the\n * intended shape; the Personal Server bounds it at 128 characters).\n *\n * @remarks\n * A payload is otherwise fully determined by\n * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical\n * requests signed inside the same second produce the same bytes. On the\n * routes that keep a replay store (the MCP and write-session handshakes\n * and delegated writes) the Personal Server refuses the second as a\n * replay; with a nonce the replay key becomes `(signer, nonce)` instead,\n * which is what lets a poll loop send the same request twice. The nonce\n * is then single use itself: re-using one is a replay even when the rest\n * of the payload changed. Plain data reads keep no replay store; their\n * exposure is bounded by the lifetime cap.\n */\n nonce?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Merged before the sort, so the nonce sits in its alphabetical place like\n // every other claim and the payload stays deterministic.\n if (params.nonce !== undefined) {\n payload[\"nonce\"] = params.nonce;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":"AAgBA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,SAAO,SAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,IAAI;AAC1B,SAAO,UAAU,WAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,QAuCxB;AAClB,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,MAAM,OAAO,OAAO;AAC1B,QAAM,MAAM,OAAO,OAAO,MAAM;AAEhC,QAAM,UAAmC;AAAA,IACvC,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO,YAAY,gBAAgB,OAAO,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd;AAEA,MAAI,OAAO,YAAY,QAAW;AAChC,YAAQ,SAAS,IAAI,OAAO;AAAA,EAC9B;AAIA,MAAI,OAAO,UAAU,QAAW;AAC9B,YAAQ,OAAO,IAAI,OAAO;AAAA,EAC5B;AAGA,QAAM,gBAAgB,OAAO,KAAK,OAAO,EACtC,KAAK,EACL,OAAgC,CAAC,KAAK,QAAQ;AAC7C,QAAI,GAAG,IAAI,QAAQ,GAAG;AACtB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEP,QAAM,cAAc,KAAK,UAAU,aAAa;AAChD,QAAM,eAAe,IAAI,YAAY,EAAE,OAAO,WAAW;AACzD,QAAM,gBAAgB,gBAAgB,YAAY;AAElD,QAAM,YAAY,MAAM,OAAO,YAAY,aAAa;AAExD,SAAO,cAAc,aAAa,IAAI,SAAS;AACjD;","names":[]}
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var web3_signed_exports = {};
20
20
  __export(web3_signed_exports, {
21
+ MAX_WEB3SIGNED_LIFETIME_SECONDS: () => MAX_WEB3SIGNED_LIFETIME_SECONDS,
21
22
  parseWeb3SignedHeader: () => parseWeb3SignedHeader,
22
23
  verifyWeb3Signed: () => verifyWeb3Signed
23
24
  });
@@ -28,6 +29,7 @@ var import_errors = require("./errors.cjs");
28
29
  var import_web3_signed_builder = require("./web3-signed-builder");
29
30
  const WEB3_SIGNED_PREFIX = "Web3Signed ";
30
31
  const CLOCK_SKEW_SECONDS = 60;
32
+ const MAX_WEB3SIGNED_LIFETIME_SECONDS = 15 * 60;
31
33
  function base64urlDecode(input) {
32
34
  let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
33
35
  const padLength = (4 - base64.length % 4) % 4;
@@ -140,10 +142,19 @@ async function verifyWeb3Signed(params) {
140
142
  if (payload.iat > now + CLOCK_SKEW_SECONDS) {
141
143
  throw new import_errors.ExpiredTokenError({ reason: "Token issued in the future" });
142
144
  }
145
+ const lifetime = payload.exp - payload.iat;
146
+ if (lifetime <= 0 || lifetime > MAX_WEB3SIGNED_LIFETIME_SECONDS) {
147
+ throw new import_errors.ExpiredTokenError({
148
+ reason: "Token lifetime exceeds the maximum",
149
+ maxLifetimeSeconds: MAX_WEB3SIGNED_LIFETIME_SECONDS,
150
+ lifetimeSeconds: lifetime
151
+ });
152
+ }
143
153
  return { signer, payload };
144
154
  }
145
155
  // Annotate the CommonJS export names for ESM import in node:
146
156
  0 && (module.exports = {
157
+ MAX_WEB3SIGNED_LIFETIME_SECONDS,
147
158
  parseWeb3SignedHeader,
148
159
  verifyWeb3Signed
149
160
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/web3-signed.ts"],"sourcesContent":["/**\n * Web3Signed Authorization header parsing and verification.\n *\n * @remarks\n * Header format: `\"Web3Signed {base64url(payload)}.{signature}\"`.\n * Payload is JSON with fields `aud`, `method`, `uri`, `bodyHash`, `iat`, `exp`,\n * and optional `grantId`. The signature is EIP-191 over the base64url-encoded\n * payload string.\n *\n * Ported from `personal-server-ts` (`packages/core/src/auth/web3-signed.ts`).\n * Adjusted to be isomorphic (no `Buffer`) and to use SDK-local error types.\n *\n * @category Auth\n */\n\nimport { recoverMessageAddress } from \"viem\";\nimport { fromBase64 } from \"../utils/encoding\";\nimport {\n MissingAuthError,\n InvalidSignatureError,\n ExpiredTokenError,\n} from \"./errors\";\nimport { computeBodyHash } from \"./web3-signed-builder\";\n\nexport interface Web3SignedPayload {\n aud: string;\n method: string;\n uri: string;\n bodyHash: string;\n iat: number;\n exp: number;\n grantId?: string;\n}\n\nexport interface VerifiedAuth {\n signer: `0x${string}`;\n payload: Web3SignedPayload;\n}\n\nconst WEB3_SIGNED_PREFIX = \"Web3Signed \";\nconst CLOCK_SKEW_SECONDS = 60;\n\n/** Decode a base64url string (no padding) to UTF-8. */\nfunction base64urlDecode(input: string): string {\n let base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padLength = (4 - (base64.length % 4)) % 4;\n base64 += \"=\".repeat(padLength);\n return new TextDecoder().decode(fromBase64(base64));\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction parsePayload(value: unknown): Web3SignedPayload {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new InvalidSignatureError({ reason: \"Invalid payload shape\" });\n }\n\n const payload = value as Record<string, unknown>;\n if (\n typeof payload[\"aud\"] !== \"string\" ||\n typeof payload[\"method\"] !== \"string\" ||\n typeof payload[\"uri\"] !== \"string\" ||\n typeof payload[\"bodyHash\"] !== \"string\" ||\n !isFiniteNumber(payload[\"iat\"]) ||\n !isFiniteNumber(payload[\"exp\"])\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid payload claims\" });\n }\n\n if (\n payload[\"grantId\"] !== undefined &&\n typeof payload[\"grantId\"] !== \"string\"\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid grantId claim\" });\n }\n\n return {\n aud: payload[\"aud\"],\n method: payload[\"method\"],\n uri: payload[\"uri\"],\n bodyHash: payload[\"bodyHash\"],\n iat: payload[\"iat\"],\n exp: payload[\"exp\"],\n grantId: payload[\"grantId\"],\n };\n}\n\n/**\n * Parse a `\"Web3Signed <base64url>.<signature>\"` header value.\n *\n * @throws {MissingAuthError} If the header is missing or empty.\n * @throws {InvalidSignatureError} If the format is invalid.\n */\nexport function parseWeb3SignedHeader(headerValue: string | undefined): {\n payloadBase64: string;\n payload: Web3SignedPayload;\n signature: `0x${string}`;\n} {\n if (!headerValue) {\n throw new MissingAuthError();\n }\n\n if (!headerValue.startsWith(WEB3_SIGNED_PREFIX)) {\n throw new InvalidSignatureError({ reason: \"Missing Web3Signed prefix\" });\n }\n\n const value = headerValue.slice(WEB3_SIGNED_PREFIX.length);\n const dotIndex = value.indexOf(\".\");\n if (dotIndex === -1 || dotIndex === 0 || dotIndex === value.length - 1) {\n throw new InvalidSignatureError({ reason: \"Invalid header format\" });\n }\n\n const payloadBase64 = value.slice(0, dotIndex);\n const signatureStr = value.slice(dotIndex + 1);\n\n if (!signatureStr.startsWith(\"0x\")) {\n throw new InvalidSignatureError({ reason: \"Invalid signature format\" });\n }\n\n let payload: Web3SignedPayload;\n try {\n const decoded = base64urlDecode(payloadBase64);\n payload = parsePayload(JSON.parse(decoded));\n } catch (err) {\n if (err instanceof InvalidSignatureError) throw err;\n throw new InvalidSignatureError({ reason: \"Invalid payload encoding\" });\n }\n\n return {\n payloadBase64,\n payload,\n signature: signatureStr as `0x${string}`,\n };\n}\n\n/**\n * Full verification: parse header, recover signer via EIP-191, check claims.\n *\n * @remarks\n * Steps:\n * 1. Parse header to base64url + signature.\n * 2. Recover signer via {@link recoverMessageAddress} (EIP-191) over the base64url payload string.\n * 3. Check `aud === expectedOrigin`, `method === expectedMethod`, `uri === expectedPath`.\n * 4. Optionally check `bodyHash` against `bodyBytes`.\n * 5. Check `iat`/`exp` within a 60s clock skew.\n *\n * @returns The recovered signer address and parsed payload.\n */\nexport async function verifyWeb3Signed(params: {\n headerValue: string | undefined;\n expectedOrigin: string;\n expectedMethod: string;\n expectedPath: string;\n bodyBytes?: Uint8Array;\n now?: number;\n}): Promise<VerifiedAuth> {\n const { payloadBase64, payload, signature } = parseWeb3SignedHeader(\n params.headerValue,\n );\n\n let signer: `0x${string}`;\n try {\n signer = await recoverMessageAddress({\n message: payloadBase64,\n signature,\n });\n } catch {\n throw new InvalidSignatureError({ reason: \"Signature recovery failed\" });\n }\n\n if (payload.aud !== params.expectedOrigin) {\n throw new InvalidSignatureError({\n reason: \"Audience mismatch\",\n expected: params.expectedOrigin,\n actual: payload.aud,\n });\n }\n\n if (payload.method !== params.expectedMethod) {\n throw new InvalidSignatureError({\n reason: \"Method mismatch\",\n expected: params.expectedMethod,\n actual: payload.method,\n });\n }\n\n if (payload.uri !== params.expectedPath) {\n throw new InvalidSignatureError({\n reason: \"URI mismatch\",\n expected: params.expectedPath,\n actual: payload.uri,\n });\n }\n\n if (params.bodyBytes !== undefined) {\n const expectedBodyHash = computeBodyHash(params.bodyBytes);\n if (payload.bodyHash !== expectedBodyHash) {\n throw new InvalidSignatureError({\n reason: \"Body hash mismatch\",\n expected: expectedBodyHash,\n actual: payload.bodyHash,\n });\n }\n }\n\n const now = params.now ?? Math.floor(Date.now() / 1000);\n\n if (payload.exp < now - CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token expired\" });\n }\n\n if (payload.iat > now + CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token issued in the future\" });\n }\n\n return { signer, payload };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,kBAAsC;AACtC,sBAA2B;AAC3B,oBAIO;AACP,iCAAgC;AAiBhC,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAG3B,SAAS,gBAAgB,OAAuB;AAC9C,MAAI,SAAS,MAAM,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACvD,QAAM,aAAa,IAAK,OAAO,SAAS,KAAM;AAC9C,YAAU,IAAI,OAAO,SAAS;AAC9B,SAAO,IAAI,YAAY,EAAE,WAAO,4BAAW,MAAM,CAAC;AACpD;AAEA,SAAS,eAAe,OAAiC;AACvD,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AAC3D;AAEA,SAAS,aAAa,OAAmC;AACvD,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACvE,UAAM,IAAI,oCAAsB,EAAE,QAAQ,wBAAwB,CAAC;AAAA,EACrE;AAEA,QAAM,UAAU;AAChB,MACE,OAAO,QAAQ,KAAK,MAAM,YAC1B,OAAO,QAAQ,QAAQ,MAAM,YAC7B,OAAO,QAAQ,KAAK,MAAM,YAC1B,OAAO,QAAQ,UAAU,MAAM,YAC/B,CAAC,eAAe,QAAQ,KAAK,CAAC,KAC9B,CAAC,eAAe,QAAQ,KAAK,CAAC,GAC9B;AACA,UAAM,IAAI,oCAAsB,EAAE,QAAQ,yBAAyB,CAAC;AAAA,EACtE;AAEA,MACE,QAAQ,SAAS,MAAM,UACvB,OAAO,QAAQ,SAAS,MAAM,UAC9B;AACA,UAAM,IAAI,oCAAsB,EAAE,QAAQ,wBAAwB,CAAC;AAAA,EACrE;AAEA,SAAO;AAAA,IACL,KAAK,QAAQ,KAAK;AAAA,IAClB,QAAQ,QAAQ,QAAQ;AAAA,IACxB,KAAK,QAAQ,KAAK;AAAA,IAClB,UAAU,QAAQ,UAAU;AAAA,IAC5B,KAAK,QAAQ,KAAK;AAAA,IAClB,KAAK,QAAQ,KAAK;AAAA,IAClB,SAAS,QAAQ,SAAS;AAAA,EAC5B;AACF;AAQO,SAAS,sBAAsB,aAIpC;AACA,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,+BAAiB;AAAA,EAC7B;AAEA,MAAI,CAAC,YAAY,WAAW,kBAAkB,GAAG;AAC/C,UAAM,IAAI,oCAAsB,EAAE,QAAQ,4BAA4B,CAAC;AAAA,EACzE;AAEA,QAAM,QAAQ,YAAY,MAAM,mBAAmB,MAAM;AACzD,QAAM,WAAW,MAAM,QAAQ,GAAG;AAClC,MAAI,aAAa,MAAM,aAAa,KAAK,aAAa,MAAM,SAAS,GAAG;AACtE,UAAM,IAAI,oCAAsB,EAAE,QAAQ,wBAAwB,CAAC;AAAA,EACrE;AAEA,QAAM,gBAAgB,MAAM,MAAM,GAAG,QAAQ;AAC7C,QAAM,eAAe,MAAM,MAAM,WAAW,CAAC;AAE7C,MAAI,CAAC,aAAa,WAAW,IAAI,GAAG;AAClC,UAAM,IAAI,oCAAsB,EAAE,QAAQ,2BAA2B,CAAC;AAAA,EACxE;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,gBAAgB,aAAa;AAC7C,cAAU,aAAa,KAAK,MAAM,OAAO,CAAC;AAAA,EAC5C,SAAS,KAAK;AACZ,QAAI,eAAe,oCAAuB,OAAM;AAChD,UAAM,IAAI,oCAAsB,EAAE,QAAQ,2BAA2B,CAAC;AAAA,EACxE;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AACF;AAeA,eAAsB,iBAAiB,QAOb;AACxB,QAAM,EAAE,eAAe,SAAS,UAAU,IAAI;AAAA,IAC5C,OAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,UAAM,mCAAsB;AAAA,MACnC,SAAS;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,UAAM,IAAI,oCAAsB,EAAE,QAAQ,4BAA4B,CAAC;AAAA,EACzE;AAEA,MAAI,QAAQ,QAAQ,OAAO,gBAAgB;AACzC,UAAM,IAAI,oCAAsB;AAAA,MAC9B,QAAQ;AAAA,MACR,UAAU,OAAO;AAAA,MACjB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,MAAI,QAAQ,WAAW,OAAO,gBAAgB;AAC5C,UAAM,IAAI,oCAAsB;AAAA,MAC9B,QAAQ;AAAA,MACR,UAAU,OAAO;AAAA,MACjB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,MAAI,QAAQ,QAAQ,OAAO,cAAc;AACvC,UAAM,IAAI,oCAAsB;AAAA,MAC9B,QAAQ;AAAA,MACR,UAAU,OAAO;AAAA,MACjB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,MAAI,OAAO,cAAc,QAAW;AAClC,UAAM,uBAAmB,4CAAgB,OAAO,SAAS;AACzD,QAAI,QAAQ,aAAa,kBAAkB;AACzC,YAAM,IAAI,oCAAsB;AAAA,QAC9B,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,QAAQ,QAAQ;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,MAAM,OAAO,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAEtD,MAAI,QAAQ,MAAM,MAAM,oBAAoB;AAC1C,UAAM,IAAI,gCAAkB,EAAE,QAAQ,gBAAgB,CAAC;AAAA,EACzD;AAEA,MAAI,QAAQ,MAAM,MAAM,oBAAoB;AAC1C,UAAM,IAAI,gCAAkB,EAAE,QAAQ,6BAA6B,CAAC;AAAA,EACtE;AAEA,SAAO,EAAE,QAAQ,QAAQ;AAC3B;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/web3-signed.ts"],"sourcesContent":["/**\n * Web3Signed Authorization header parsing and verification.\n *\n * @remarks\n * Header format: `\"Web3Signed {base64url(payload)}.{signature}\"`.\n * Payload is JSON with fields `aud`, `method`, `uri`, `bodyHash`, `iat`, `exp`,\n * and optional `grantId`. The signature is EIP-191 over the base64url-encoded\n * payload string.\n *\n * Ported from `personal-server-ts` (`packages/core/src/auth/web3-signed.ts`).\n * Adjusted to be isomorphic (no `Buffer`) and to use SDK-local error types.\n *\n * @category Auth\n */\n\nimport { recoverMessageAddress } from \"viem\";\nimport { fromBase64 } from \"../utils/encoding\";\nimport {\n MissingAuthError,\n InvalidSignatureError,\n ExpiredTokenError,\n} from \"./errors\";\nimport { computeBodyHash } from \"./web3-signed-builder\";\n\nexport interface Web3SignedPayload {\n aud: string;\n method: string;\n uri: string;\n bodyHash: string;\n iat: number;\n exp: number;\n grantId?: string;\n}\n\nexport interface VerifiedAuth {\n signer: `0x${string}`;\n payload: Web3SignedPayload;\n}\n\nconst WEB3_SIGNED_PREFIX = \"Web3Signed \";\nconst CLOCK_SKEW_SECONDS = 60;\n\n/**\n * Longest `exp - iat` window a Web3Signed header may carry (seconds). Without\n * this bound the signer chooses how long a captured request stays replayable.\n *\n * @category Auth\n */\nexport const MAX_WEB3SIGNED_LIFETIME_SECONDS = 15 * 60;\n\n/** Decode a base64url string (no padding) to UTF-8. */\nfunction base64urlDecode(input: string): string {\n let base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padLength = (4 - (base64.length % 4)) % 4;\n base64 += \"=\".repeat(padLength);\n return new TextDecoder().decode(fromBase64(base64));\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction parsePayload(value: unknown): Web3SignedPayload {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new InvalidSignatureError({ reason: \"Invalid payload shape\" });\n }\n\n const payload = value as Record<string, unknown>;\n if (\n typeof payload[\"aud\"] !== \"string\" ||\n typeof payload[\"method\"] !== \"string\" ||\n typeof payload[\"uri\"] !== \"string\" ||\n typeof payload[\"bodyHash\"] !== \"string\" ||\n !isFiniteNumber(payload[\"iat\"]) ||\n !isFiniteNumber(payload[\"exp\"])\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid payload claims\" });\n }\n\n if (\n payload[\"grantId\"] !== undefined &&\n typeof payload[\"grantId\"] !== \"string\"\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid grantId claim\" });\n }\n\n return {\n aud: payload[\"aud\"],\n method: payload[\"method\"],\n uri: payload[\"uri\"],\n bodyHash: payload[\"bodyHash\"],\n iat: payload[\"iat\"],\n exp: payload[\"exp\"],\n grantId: payload[\"grantId\"],\n };\n}\n\n/**\n * Parse a `\"Web3Signed <base64url>.<signature>\"` header value.\n *\n * @throws {MissingAuthError} If the header is missing or empty.\n * @throws {InvalidSignatureError} If the format is invalid.\n */\nexport function parseWeb3SignedHeader(headerValue: string | undefined): {\n payloadBase64: string;\n payload: Web3SignedPayload;\n signature: `0x${string}`;\n} {\n if (!headerValue) {\n throw new MissingAuthError();\n }\n\n if (!headerValue.startsWith(WEB3_SIGNED_PREFIX)) {\n throw new InvalidSignatureError({ reason: \"Missing Web3Signed prefix\" });\n }\n\n const value = headerValue.slice(WEB3_SIGNED_PREFIX.length);\n const dotIndex = value.indexOf(\".\");\n if (dotIndex === -1 || dotIndex === 0 || dotIndex === value.length - 1) {\n throw new InvalidSignatureError({ reason: \"Invalid header format\" });\n }\n\n const payloadBase64 = value.slice(0, dotIndex);\n const signatureStr = value.slice(dotIndex + 1);\n\n if (!signatureStr.startsWith(\"0x\")) {\n throw new InvalidSignatureError({ reason: \"Invalid signature format\" });\n }\n\n let payload: Web3SignedPayload;\n try {\n const decoded = base64urlDecode(payloadBase64);\n payload = parsePayload(JSON.parse(decoded));\n } catch (err) {\n if (err instanceof InvalidSignatureError) throw err;\n throw new InvalidSignatureError({ reason: \"Invalid payload encoding\" });\n }\n\n return {\n payloadBase64,\n payload,\n signature: signatureStr as `0x${string}`,\n };\n}\n\n/**\n * Full verification: parse header, recover signer via EIP-191, check claims.\n *\n * @remarks\n * Steps:\n * 1. Parse header to base64url + signature.\n * 2. Recover signer via {@link recoverMessageAddress} (EIP-191) over the base64url payload string.\n * 3. Check `aud === expectedOrigin`, `method === expectedMethod`, `uri === expectedPath`.\n * 4. Optionally check `bodyHash` against `bodyBytes`.\n * 5. Check `iat`/`exp` within a 60s clock skew.\n *\n * @returns The recovered signer address and parsed payload.\n */\nexport async function verifyWeb3Signed(params: {\n headerValue: string | undefined;\n expectedOrigin: string;\n expectedMethod: string;\n expectedPath: string;\n bodyBytes?: Uint8Array;\n now?: number;\n}): Promise<VerifiedAuth> {\n const { payloadBase64, payload, signature } = parseWeb3SignedHeader(\n params.headerValue,\n );\n\n let signer: `0x${string}`;\n try {\n signer = await recoverMessageAddress({\n message: payloadBase64,\n signature,\n });\n } catch {\n throw new InvalidSignatureError({ reason: \"Signature recovery failed\" });\n }\n\n if (payload.aud !== params.expectedOrigin) {\n throw new InvalidSignatureError({\n reason: \"Audience mismatch\",\n expected: params.expectedOrigin,\n actual: payload.aud,\n });\n }\n\n if (payload.method !== params.expectedMethod) {\n throw new InvalidSignatureError({\n reason: \"Method mismatch\",\n expected: params.expectedMethod,\n actual: payload.method,\n });\n }\n\n if (payload.uri !== params.expectedPath) {\n throw new InvalidSignatureError({\n reason: \"URI mismatch\",\n expected: params.expectedPath,\n actual: payload.uri,\n });\n }\n\n if (params.bodyBytes !== undefined) {\n const expectedBodyHash = computeBodyHash(params.bodyBytes);\n if (payload.bodyHash !== expectedBodyHash) {\n throw new InvalidSignatureError({\n reason: \"Body hash mismatch\",\n expected: expectedBodyHash,\n actual: payload.bodyHash,\n });\n }\n }\n\n const now = params.now ?? Math.floor(Date.now() / 1000);\n\n if (payload.exp < now - CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token expired\" });\n }\n\n if (payload.iat > now + CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token issued in the future\" });\n }\n\n // Bound the window the signer chose, not just where `now` falls in it:\n // otherwise a captured request stays replayable for as long as the signer\n // liked.\n const lifetime = payload.exp - payload.iat;\n if (lifetime <= 0 || lifetime > MAX_WEB3SIGNED_LIFETIME_SECONDS) {\n throw new ExpiredTokenError({\n reason: \"Token lifetime exceeds the maximum\",\n maxLifetimeSeconds: MAX_WEB3SIGNED_LIFETIME_SECONDS,\n lifetimeSeconds: lifetime,\n });\n }\n\n return { signer, payload };\n}\n"],"mappings":"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","names":[]}
@@ -25,6 +25,13 @@ export interface VerifiedAuth {
25
25
  signer: `0x${string}`;
26
26
  payload: Web3SignedPayload;
27
27
  }
28
+ /**
29
+ * Longest `exp - iat` window a Web3Signed header may carry (seconds). Without
30
+ * this bound the signer chooses how long a captured request stays replayable.
31
+ *
32
+ * @category Auth
33
+ */
34
+ export declare const MAX_WEB3SIGNED_LIFETIME_SECONDS: number;
28
35
  /**
29
36
  * Parse a `"Web3Signed <base64url>.<signature>"` header value.
30
37
  *
@@ -8,6 +8,7 @@ import {
8
8
  import { computeBodyHash } from "./web3-signed-builder.js";
9
9
  const WEB3_SIGNED_PREFIX = "Web3Signed ";
10
10
  const CLOCK_SKEW_SECONDS = 60;
11
+ const MAX_WEB3SIGNED_LIFETIME_SECONDS = 15 * 60;
11
12
  function base64urlDecode(input) {
12
13
  let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
13
14
  const padLength = (4 - base64.length % 4) % 4;
@@ -120,9 +121,18 @@ async function verifyWeb3Signed(params) {
120
121
  if (payload.iat > now + CLOCK_SKEW_SECONDS) {
121
122
  throw new ExpiredTokenError({ reason: "Token issued in the future" });
122
123
  }
124
+ const lifetime = payload.exp - payload.iat;
125
+ if (lifetime <= 0 || lifetime > MAX_WEB3SIGNED_LIFETIME_SECONDS) {
126
+ throw new ExpiredTokenError({
127
+ reason: "Token lifetime exceeds the maximum",
128
+ maxLifetimeSeconds: MAX_WEB3SIGNED_LIFETIME_SECONDS,
129
+ lifetimeSeconds: lifetime
130
+ });
131
+ }
123
132
  return { signer, payload };
124
133
  }
125
134
  export {
135
+ MAX_WEB3SIGNED_LIFETIME_SECONDS,
126
136
  parseWeb3SignedHeader,
127
137
  verifyWeb3Signed
128
138
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/web3-signed.ts"],"sourcesContent":["/**\n * Web3Signed Authorization header parsing and verification.\n *\n * @remarks\n * Header format: `\"Web3Signed {base64url(payload)}.{signature}\"`.\n * Payload is JSON with fields `aud`, `method`, `uri`, `bodyHash`, `iat`, `exp`,\n * and optional `grantId`. The signature is EIP-191 over the base64url-encoded\n * payload string.\n *\n * Ported from `personal-server-ts` (`packages/core/src/auth/web3-signed.ts`).\n * Adjusted to be isomorphic (no `Buffer`) and to use SDK-local error types.\n *\n * @category Auth\n */\n\nimport { recoverMessageAddress } from \"viem\";\nimport { fromBase64 } from \"../utils/encoding\";\nimport {\n MissingAuthError,\n InvalidSignatureError,\n ExpiredTokenError,\n} from \"./errors\";\nimport { computeBodyHash } from \"./web3-signed-builder\";\n\nexport interface Web3SignedPayload {\n aud: string;\n method: string;\n uri: string;\n bodyHash: string;\n iat: number;\n exp: number;\n grantId?: string;\n}\n\nexport interface VerifiedAuth {\n signer: `0x${string}`;\n payload: Web3SignedPayload;\n}\n\nconst WEB3_SIGNED_PREFIX = \"Web3Signed \";\nconst CLOCK_SKEW_SECONDS = 60;\n\n/** Decode a base64url string (no padding) to UTF-8. */\nfunction base64urlDecode(input: string): string {\n let base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padLength = (4 - (base64.length % 4)) % 4;\n base64 += \"=\".repeat(padLength);\n return new TextDecoder().decode(fromBase64(base64));\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction parsePayload(value: unknown): Web3SignedPayload {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new InvalidSignatureError({ reason: \"Invalid payload shape\" });\n }\n\n const payload = value as Record<string, unknown>;\n if (\n typeof payload[\"aud\"] !== \"string\" ||\n typeof payload[\"method\"] !== \"string\" ||\n typeof payload[\"uri\"] !== \"string\" ||\n typeof payload[\"bodyHash\"] !== \"string\" ||\n !isFiniteNumber(payload[\"iat\"]) ||\n !isFiniteNumber(payload[\"exp\"])\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid payload claims\" });\n }\n\n if (\n payload[\"grantId\"] !== undefined &&\n typeof payload[\"grantId\"] !== \"string\"\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid grantId claim\" });\n }\n\n return {\n aud: payload[\"aud\"],\n method: payload[\"method\"],\n uri: payload[\"uri\"],\n bodyHash: payload[\"bodyHash\"],\n iat: payload[\"iat\"],\n exp: payload[\"exp\"],\n grantId: payload[\"grantId\"],\n };\n}\n\n/**\n * Parse a `\"Web3Signed <base64url>.<signature>\"` header value.\n *\n * @throws {MissingAuthError} If the header is missing or empty.\n * @throws {InvalidSignatureError} If the format is invalid.\n */\nexport function parseWeb3SignedHeader(headerValue: string | undefined): {\n payloadBase64: string;\n payload: Web3SignedPayload;\n signature: `0x${string}`;\n} {\n if (!headerValue) {\n throw new MissingAuthError();\n }\n\n if (!headerValue.startsWith(WEB3_SIGNED_PREFIX)) {\n throw new InvalidSignatureError({ reason: \"Missing Web3Signed prefix\" });\n }\n\n const value = headerValue.slice(WEB3_SIGNED_PREFIX.length);\n const dotIndex = value.indexOf(\".\");\n if (dotIndex === -1 || dotIndex === 0 || dotIndex === value.length - 1) {\n throw new InvalidSignatureError({ reason: \"Invalid header format\" });\n }\n\n const payloadBase64 = value.slice(0, dotIndex);\n const signatureStr = value.slice(dotIndex + 1);\n\n if (!signatureStr.startsWith(\"0x\")) {\n throw new InvalidSignatureError({ reason: \"Invalid signature format\" });\n }\n\n let payload: Web3SignedPayload;\n try {\n const decoded = base64urlDecode(payloadBase64);\n payload = parsePayload(JSON.parse(decoded));\n } catch (err) {\n if (err instanceof InvalidSignatureError) throw err;\n throw new InvalidSignatureError({ reason: \"Invalid payload encoding\" });\n }\n\n return {\n payloadBase64,\n payload,\n signature: signatureStr as `0x${string}`,\n };\n}\n\n/**\n * Full verification: parse header, recover signer via EIP-191, check claims.\n *\n * @remarks\n * Steps:\n * 1. Parse header to base64url + signature.\n * 2. Recover signer via {@link recoverMessageAddress} (EIP-191) over the base64url payload string.\n * 3. Check `aud === expectedOrigin`, `method === expectedMethod`, `uri === expectedPath`.\n * 4. Optionally check `bodyHash` against `bodyBytes`.\n * 5. Check `iat`/`exp` within a 60s clock skew.\n *\n * @returns The recovered signer address and parsed payload.\n */\nexport async function verifyWeb3Signed(params: {\n headerValue: string | undefined;\n expectedOrigin: string;\n expectedMethod: string;\n expectedPath: string;\n bodyBytes?: Uint8Array;\n now?: number;\n}): Promise<VerifiedAuth> {\n const { payloadBase64, payload, signature } = parseWeb3SignedHeader(\n params.headerValue,\n );\n\n let signer: `0x${string}`;\n try {\n signer = await recoverMessageAddress({\n message: payloadBase64,\n signature,\n });\n } catch {\n throw new InvalidSignatureError({ reason: \"Signature recovery failed\" });\n }\n\n if (payload.aud !== params.expectedOrigin) {\n throw new InvalidSignatureError({\n reason: \"Audience mismatch\",\n expected: params.expectedOrigin,\n actual: payload.aud,\n });\n }\n\n if (payload.method !== params.expectedMethod) {\n throw new InvalidSignatureError({\n reason: \"Method mismatch\",\n expected: params.expectedMethod,\n actual: payload.method,\n });\n }\n\n if (payload.uri !== params.expectedPath) {\n throw new InvalidSignatureError({\n reason: \"URI mismatch\",\n expected: params.expectedPath,\n actual: payload.uri,\n });\n }\n\n if (params.bodyBytes !== undefined) {\n const expectedBodyHash = computeBodyHash(params.bodyBytes);\n if (payload.bodyHash !== expectedBodyHash) {\n throw new InvalidSignatureError({\n reason: \"Body hash mismatch\",\n expected: expectedBodyHash,\n actual: payload.bodyHash,\n });\n }\n }\n\n const now = params.now ?? Math.floor(Date.now() / 1000);\n\n if (payload.exp < now - CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token expired\" });\n }\n\n if (payload.iat > now + CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token issued in the future\" });\n }\n\n return { signer, payload };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/auth/web3-signed.ts"],"sourcesContent":["/**\n * Web3Signed Authorization header parsing and verification.\n *\n * @remarks\n * Header format: `\"Web3Signed {base64url(payload)}.{signature}\"`.\n * Payload is JSON with fields `aud`, `method`, `uri`, `bodyHash`, `iat`, `exp`,\n * and optional `grantId`. The signature is EIP-191 over the base64url-encoded\n * payload string.\n *\n * Ported from `personal-server-ts` (`packages/core/src/auth/web3-signed.ts`).\n * Adjusted to be isomorphic (no `Buffer`) and to use SDK-local error types.\n *\n * @category Auth\n */\n\nimport { recoverMessageAddress } from \"viem\";\nimport { fromBase64 } from \"../utils/encoding\";\nimport {\n MissingAuthError,\n InvalidSignatureError,\n ExpiredTokenError,\n} from \"./errors\";\nimport { computeBodyHash } from \"./web3-signed-builder\";\n\nexport interface Web3SignedPayload {\n aud: string;\n method: string;\n uri: string;\n bodyHash: string;\n iat: number;\n exp: number;\n grantId?: string;\n}\n\nexport interface VerifiedAuth {\n signer: `0x${string}`;\n payload: Web3SignedPayload;\n}\n\nconst WEB3_SIGNED_PREFIX = \"Web3Signed \";\nconst CLOCK_SKEW_SECONDS = 60;\n\n/**\n * Longest `exp - iat` window a Web3Signed header may carry (seconds). Without\n * this bound the signer chooses how long a captured request stays replayable.\n *\n * @category Auth\n */\nexport const MAX_WEB3SIGNED_LIFETIME_SECONDS = 15 * 60;\n\n/** Decode a base64url string (no padding) to UTF-8. */\nfunction base64urlDecode(input: string): string {\n let base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padLength = (4 - (base64.length % 4)) % 4;\n base64 += \"=\".repeat(padLength);\n return new TextDecoder().decode(fromBase64(base64));\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction parsePayload(value: unknown): Web3SignedPayload {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new InvalidSignatureError({ reason: \"Invalid payload shape\" });\n }\n\n const payload = value as Record<string, unknown>;\n if (\n typeof payload[\"aud\"] !== \"string\" ||\n typeof payload[\"method\"] !== \"string\" ||\n typeof payload[\"uri\"] !== \"string\" ||\n typeof payload[\"bodyHash\"] !== \"string\" ||\n !isFiniteNumber(payload[\"iat\"]) ||\n !isFiniteNumber(payload[\"exp\"])\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid payload claims\" });\n }\n\n if (\n payload[\"grantId\"] !== undefined &&\n typeof payload[\"grantId\"] !== \"string\"\n ) {\n throw new InvalidSignatureError({ reason: \"Invalid grantId claim\" });\n }\n\n return {\n aud: payload[\"aud\"],\n method: payload[\"method\"],\n uri: payload[\"uri\"],\n bodyHash: payload[\"bodyHash\"],\n iat: payload[\"iat\"],\n exp: payload[\"exp\"],\n grantId: payload[\"grantId\"],\n };\n}\n\n/**\n * Parse a `\"Web3Signed <base64url>.<signature>\"` header value.\n *\n * @throws {MissingAuthError} If the header is missing or empty.\n * @throws {InvalidSignatureError} If the format is invalid.\n */\nexport function parseWeb3SignedHeader(headerValue: string | undefined): {\n payloadBase64: string;\n payload: Web3SignedPayload;\n signature: `0x${string}`;\n} {\n if (!headerValue) {\n throw new MissingAuthError();\n }\n\n if (!headerValue.startsWith(WEB3_SIGNED_PREFIX)) {\n throw new InvalidSignatureError({ reason: \"Missing Web3Signed prefix\" });\n }\n\n const value = headerValue.slice(WEB3_SIGNED_PREFIX.length);\n const dotIndex = value.indexOf(\".\");\n if (dotIndex === -1 || dotIndex === 0 || dotIndex === value.length - 1) {\n throw new InvalidSignatureError({ reason: \"Invalid header format\" });\n }\n\n const payloadBase64 = value.slice(0, dotIndex);\n const signatureStr = value.slice(dotIndex + 1);\n\n if (!signatureStr.startsWith(\"0x\")) {\n throw new InvalidSignatureError({ reason: \"Invalid signature format\" });\n }\n\n let payload: Web3SignedPayload;\n try {\n const decoded = base64urlDecode(payloadBase64);\n payload = parsePayload(JSON.parse(decoded));\n } catch (err) {\n if (err instanceof InvalidSignatureError) throw err;\n throw new InvalidSignatureError({ reason: \"Invalid payload encoding\" });\n }\n\n return {\n payloadBase64,\n payload,\n signature: signatureStr as `0x${string}`,\n };\n}\n\n/**\n * Full verification: parse header, recover signer via EIP-191, check claims.\n *\n * @remarks\n * Steps:\n * 1. Parse header to base64url + signature.\n * 2. Recover signer via {@link recoverMessageAddress} (EIP-191) over the base64url payload string.\n * 3. Check `aud === expectedOrigin`, `method === expectedMethod`, `uri === expectedPath`.\n * 4. Optionally check `bodyHash` against `bodyBytes`.\n * 5. Check `iat`/`exp` within a 60s clock skew.\n *\n * @returns The recovered signer address and parsed payload.\n */\nexport async function verifyWeb3Signed(params: {\n headerValue: string | undefined;\n expectedOrigin: string;\n expectedMethod: string;\n expectedPath: string;\n bodyBytes?: Uint8Array;\n now?: number;\n}): Promise<VerifiedAuth> {\n const { payloadBase64, payload, signature } = parseWeb3SignedHeader(\n params.headerValue,\n );\n\n let signer: `0x${string}`;\n try {\n signer = await recoverMessageAddress({\n message: payloadBase64,\n signature,\n });\n } catch {\n throw new InvalidSignatureError({ reason: \"Signature recovery failed\" });\n }\n\n if (payload.aud !== params.expectedOrigin) {\n throw new InvalidSignatureError({\n reason: \"Audience mismatch\",\n expected: params.expectedOrigin,\n actual: payload.aud,\n });\n }\n\n if (payload.method !== params.expectedMethod) {\n throw new InvalidSignatureError({\n reason: \"Method mismatch\",\n expected: params.expectedMethod,\n actual: payload.method,\n });\n }\n\n if (payload.uri !== params.expectedPath) {\n throw new InvalidSignatureError({\n reason: \"URI mismatch\",\n expected: params.expectedPath,\n actual: payload.uri,\n });\n }\n\n if (params.bodyBytes !== undefined) {\n const expectedBodyHash = computeBodyHash(params.bodyBytes);\n if (payload.bodyHash !== expectedBodyHash) {\n throw new InvalidSignatureError({\n reason: \"Body hash mismatch\",\n expected: expectedBodyHash,\n actual: payload.bodyHash,\n });\n }\n }\n\n const now = params.now ?? Math.floor(Date.now() / 1000);\n\n if (payload.exp < now - CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token expired\" });\n }\n\n if (payload.iat > now + CLOCK_SKEW_SECONDS) {\n throw new ExpiredTokenError({ reason: \"Token issued in the future\" });\n }\n\n // Bound the window the signer chose, not just where `now` falls in it:\n // otherwise a captured request stays replayable for as long as the signer\n // liked.\n const lifetime = payload.exp - payload.iat;\n if (lifetime <= 0 || lifetime > MAX_WEB3SIGNED_LIFETIME_SECONDS) {\n throw new ExpiredTokenError({\n reason: \"Token lifetime exceeds the maximum\",\n maxLifetimeSeconds: MAX_WEB3SIGNED_LIFETIME_SECONDS,\n lifetimeSeconds: lifetime,\n });\n }\n\n return { signer, payload };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var account_origin_exports = {};
20
+ __export(account_origin_exports, {
21
+ parseOriginAllowlist: () => parseOriginAllowlist,
22
+ resolveVanaAccountOrigin: () => resolveVanaAccountOrigin
23
+ });
24
+ module.exports = __toCommonJS(account_origin_exports);
25
+ var import_protocol = require("./protocol");
26
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
27
+ const HOSTNAME = /^(?:\[[0-9a-f:.]+\]|[a-z0-9-]+(?:\.[a-z0-9-]+)*)$/;
28
+ function exactOrigin(value, protocol) {
29
+ let url;
30
+ try {
31
+ url = new URL(value);
32
+ } catch {
33
+ return null;
34
+ }
35
+ return url.protocol === protocol && url.origin === value && HOSTNAME.test(url.hostname) ? value : null;
36
+ }
37
+ function parseOriginAllowlist(csv) {
38
+ if (!csv) return [];
39
+ const origins = [];
40
+ for (const entry of csv.split(",")) {
41
+ const origin = exactOrigin(entry.trim(), "https:");
42
+ if (origin && !origins.includes(origin)) origins.push(origin);
43
+ }
44
+ return origins;
45
+ }
46
+ function resolveVanaAccountOrigin(input) {
47
+ if (input.env !== "production" && input.env !== "dev") {
48
+ throw new TypeError('env must be "production" or "dev"');
49
+ }
50
+ const fallback = import_protocol.VANA_ACCOUNT_ORIGINS[input.env];
51
+ const { override } = input;
52
+ if (input.allowOverride !== true || typeof override !== "string") {
53
+ return fallback;
54
+ }
55
+ const https = exactOrigin(override, "https:");
56
+ if (https && (input.previewOrigins ?? []).includes(https)) {
57
+ return https;
58
+ }
59
+ if (input.allowLocal === true) {
60
+ const local = exactOrigin(override, "http:");
61
+ if (local && LOOPBACK_HOSTS.has(new URL(local).hostname)) {
62
+ return local;
63
+ }
64
+ }
65
+ return fallback;
66
+ }
67
+ // Annotate the CommonJS export names for ESM import in node:
68
+ 0 && (module.exports = {
69
+ parseOriginAllowlist,
70
+ resolveVanaAccountOrigin
71
+ });
72
+ //# sourceMappingURL=account-origin.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/connect/account-origin.ts"],"sourcesContent":["/**\n * Which Vana Account origin serves the connect sheet.\n *\n * @remarks\n * The sheet origin decides who the person signs in to and who receives the\n * partner's request capability, so it must never come from a pattern the\n * page URL can satisfy. {@link resolveVanaAccountOrigin} returns the\n * environment's Vana Account origin and accepts an override only when the\n * caller switched overrides on and the override is an exact origin from an\n * explicit allowlist. There is deliberately no pattern or wildcard option.\n *\n * Zero-dependency and browser-safe, like the protocol it builds on.\n *\n * @category Connect\n * @module connect/account-origin\n */\n\nimport { VANA_ACCOUNT_ORIGINS, type VanaConnectEnv } from \"./protocol\";\n\n/** Input for {@link resolveVanaAccountOrigin}. */\nexport interface ResolveVanaAccountOriginInput {\n /** The deployment environment the app talks to. Required: there is no default. */\n env: VanaConnectEnv;\n /**\n * A requested Account origin, typically read from a query parameter on a\n * preview deploy. Untrusted. Ignored unless {@link allowOverride} is true.\n */\n override?: string | null;\n /**\n * Exact `https` origins (`https://host[:port]`, no path, no trailing slash)\n * an override may name. Build it with {@link parseOriginAllowlist}. Entries\n * that are not exact https origins never match.\n */\n previewOrigins?: readonly string[];\n /**\n * Whether this build honours an override at all. Decide it from build-time\n * configuration (a non-production deploy), never from the request. Keep it\n * `false` in production.\n */\n allowOverride: boolean;\n /**\n * Also accept an exact `http` loopback origin (`localhost`, `127.0.0.1`,\n * `[::1]`, any port) without listing it, for a locally run Vana Account.\n * Only applies when {@link allowOverride} is true. Defaults to `false`.\n */\n allowLocal?: boolean;\n}\n\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n/** DNS labels (punycode included) or a bracketed IPv6 literal; no wildcards. */\nconst HOSTNAME = /^(?:\\[[0-9a-f:.]+\\]|[a-z0-9-]+(?:\\.[a-z0-9-]+)*)$/;\n\n/** `value` itself when it is a bare origin with `protocol`, else `null`. */\nfunction exactOrigin(\n value: string,\n protocol: \"https:\" | \"http:\",\n): string | null {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return null;\n }\n // `url.origin === value` rejects paths, queries, fragments, credentials,\n // trailing slashes and any spelling the URL parser would rewrite.\n return url.protocol === protocol &&\n url.origin === value &&\n HOSTNAME.test(url.hostname)\n ? value\n : null;\n}\n\n/**\n * Parse a comma-separated allowlist of exact `https` origins.\n *\n * @remarks\n * Whitespace around entries is trimmed. An entry that is not an exact\n * `https://host[:port]` origin (a path, a trailing slash, `http:`, a\n * wildcard such as `https://*.vercel.app`) is dropped, so a typo narrows\n * the list instead of widening it.\n *\n * @example\n * ```typescript\n * parseOriginAllowlist(process.env.VANA_ACCOUNT_PREVIEW_ORIGINS);\n * // [\"https://account-git-my-branch.vercel.app\"]\n * ```\n *\n * @param csv - The raw list, for example an environment variable.\n * @returns The exact https origins, deduplicated, in order.\n */\nexport function parseOriginAllowlist(csv: string | null | undefined): string[] {\n if (!csv) return [];\n const origins: string[] = [];\n for (const entry of csv.split(\",\")) {\n const origin = exactOrigin(entry.trim(), \"https:\");\n if (origin && !origins.includes(origin)) origins.push(origin);\n }\n return origins;\n}\n\n/**\n * Resolve the Vana Account origin that serves the connect sheet.\n *\n * @remarks\n * Returns `VANA_ACCOUNT_ORIGINS[env]` unless `allowOverride` is true and\n * `override` is exactly one of `previewOrigins` (an `https` origin), or,\n * with `allowLocal`, exactly an `http` loopback origin. Anything else (a\n * path, a different scheme, a near-miss host, a lookalike) falls back to the\n * environment's origin. Matching is exact string equality: no patterns, no\n * suffixes, no normalization that could widen a match.\n *\n * @example\n * ```typescript\n * const accountOrigin = resolveVanaAccountOrigin({\n * env: \"dev\",\n * override: new URLSearchParams(location.search).get(\"vana_account\"),\n * previewOrigins: parseOriginAllowlist(\n * process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,\n * ),\n * allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === \"preview\",\n * });\n * ```\n *\n * @param input - Environment, untrusted override, allowlist and switches.\n * @returns A bare Vana Account origin.\n * @throws TypeError when `env` is not `\"production\"` or `\"dev\"`.\n */\nexport function resolveVanaAccountOrigin(\n input: ResolveVanaAccountOriginInput,\n): string {\n if (input.env !== \"production\" && input.env !== \"dev\") {\n throw new TypeError('env must be \"production\" or \"dev\"');\n }\n const fallback: string = VANA_ACCOUNT_ORIGINS[input.env];\n const { override } = input;\n if (input.allowOverride !== true || typeof override !== \"string\") {\n return fallback;\n }\n const https = exactOrigin(override, \"https:\");\n if (https && (input.previewOrigins ?? []).includes(https)) {\n return https;\n }\n if (input.allowLocal === true) {\n const local = exactOrigin(override, \"http:\");\n if (local && LOOPBACK_HOSTS.has(new URL(local).hostname)) {\n return local;\n }\n }\n return fallback;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,sBAA0D;AA+B1D,MAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAElE,MAAM,WAAW;AAGjB,SAAS,YACP,OACA,UACe;AACf,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,SAAO,IAAI,aAAa,YACtB,IAAI,WAAW,SACf,SAAS,KAAK,IAAI,QAAQ,IACxB,QACA;AACN;AAoBO,SAAS,qBAAqB,KAA0C;AAC7E,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAM,UAAoB,CAAC;AAC3B,aAAW,SAAS,IAAI,MAAM,GAAG,GAAG;AAClC,UAAM,SAAS,YAAY,MAAM,KAAK,GAAG,QAAQ;AACjD,QAAI,UAAU,CAAC,QAAQ,SAAS,MAAM,EAAG,SAAQ,KAAK,MAAM;AAAA,EAC9D;AACA,SAAO;AACT;AA6BO,SAAS,yBACd,OACQ;AACR,MAAI,MAAM,QAAQ,gBAAgB,MAAM,QAAQ,OAAO;AACrD,UAAM,IAAI,UAAU,mCAAmC;AAAA,EACzD;AACA,QAAM,WAAmB,qCAAqB,MAAM,GAAG;AACvD,QAAM,EAAE,SAAS,IAAI;AACrB,MAAI,MAAM,kBAAkB,QAAQ,OAAO,aAAa,UAAU;AAChE,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,YAAY,UAAU,QAAQ;AAC5C,MAAI,UAAU,MAAM,kBAAkB,CAAC,GAAG,SAAS,KAAK,GAAG;AACzD,WAAO;AAAA,EACT;AACA,MAAI,MAAM,eAAe,MAAM;AAC7B,UAAM,QAAQ,YAAY,UAAU,OAAO;AAC3C,QAAI,SAAS,eAAe,IAAI,IAAI,IAAI,KAAK,EAAE,QAAQ,GAAG;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Which Vana Account origin serves the connect sheet.
3
+ *
4
+ * @remarks
5
+ * The sheet origin decides who the person signs in to and who receives the
6
+ * partner's request capability, so it must never come from a pattern the
7
+ * page URL can satisfy. {@link resolveVanaAccountOrigin} returns the
8
+ * environment's Vana Account origin and accepts an override only when the
9
+ * caller switched overrides on and the override is an exact origin from an
10
+ * explicit allowlist. There is deliberately no pattern or wildcard option.
11
+ *
12
+ * Zero-dependency and browser-safe, like the protocol it builds on.
13
+ *
14
+ * @category Connect
15
+ * @module connect/account-origin
16
+ */
17
+ import { type VanaConnectEnv } from "./protocol.js";
18
+ /** Input for {@link resolveVanaAccountOrigin}. */
19
+ export interface ResolveVanaAccountOriginInput {
20
+ /** The deployment environment the app talks to. Required: there is no default. */
21
+ env: VanaConnectEnv;
22
+ /**
23
+ * A requested Account origin, typically read from a query parameter on a
24
+ * preview deploy. Untrusted. Ignored unless {@link allowOverride} is true.
25
+ */
26
+ override?: string | null;
27
+ /**
28
+ * Exact `https` origins (`https://host[:port]`, no path, no trailing slash)
29
+ * an override may name. Build it with {@link parseOriginAllowlist}. Entries
30
+ * that are not exact https origins never match.
31
+ */
32
+ previewOrigins?: readonly string[];
33
+ /**
34
+ * Whether this build honours an override at all. Decide it from build-time
35
+ * configuration (a non-production deploy), never from the request. Keep it
36
+ * `false` in production.
37
+ */
38
+ allowOverride: boolean;
39
+ /**
40
+ * Also accept an exact `http` loopback origin (`localhost`, `127.0.0.1`,
41
+ * `[::1]`, any port) without listing it, for a locally run Vana Account.
42
+ * Only applies when {@link allowOverride} is true. Defaults to `false`.
43
+ */
44
+ allowLocal?: boolean;
45
+ }
46
+ /**
47
+ * Parse a comma-separated allowlist of exact `https` origins.
48
+ *
49
+ * @remarks
50
+ * Whitespace around entries is trimmed. An entry that is not an exact
51
+ * `https://host[:port]` origin (a path, a trailing slash, `http:`, a
52
+ * wildcard such as `https://*.vercel.app`) is dropped, so a typo narrows
53
+ * the list instead of widening it.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * parseOriginAllowlist(process.env.VANA_ACCOUNT_PREVIEW_ORIGINS);
58
+ * // ["https://account-git-my-branch.vercel.app"]
59
+ * ```
60
+ *
61
+ * @param csv - The raw list, for example an environment variable.
62
+ * @returns The exact https origins, deduplicated, in order.
63
+ */
64
+ export declare function parseOriginAllowlist(csv: string | null | undefined): string[];
65
+ /**
66
+ * Resolve the Vana Account origin that serves the connect sheet.
67
+ *
68
+ * @remarks
69
+ * Returns `VANA_ACCOUNT_ORIGINS[env]` unless `allowOverride` is true and
70
+ * `override` is exactly one of `previewOrigins` (an `https` origin), or,
71
+ * with `allowLocal`, exactly an `http` loopback origin. Anything else (a
72
+ * path, a different scheme, a near-miss host, a lookalike) falls back to the
73
+ * environment's origin. Matching is exact string equality: no patterns, no
74
+ * suffixes, no normalization that could widen a match.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const accountOrigin = resolveVanaAccountOrigin({
79
+ * env: "dev",
80
+ * override: new URLSearchParams(location.search).get("vana_account"),
81
+ * previewOrigins: parseOriginAllowlist(
82
+ * process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,
83
+ * ),
84
+ * allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === "preview",
85
+ * });
86
+ * ```
87
+ *
88
+ * @param input - Environment, untrusted override, allowlist and switches.
89
+ * @returns A bare Vana Account origin.
90
+ * @throws TypeError when `env` is not `"production"` or `"dev"`.
91
+ */
92
+ export declare function resolveVanaAccountOrigin(input: ResolveVanaAccountOriginInput): string;