@opendatalabs/vana-sdk 4.3.0 → 4.3.2
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.
- package/dist/auth/web3-signed-builder.cjs.map +1 -1
- package/dist/auth/web3-signed-builder.d.ts +12 -6
- package/dist/auth/web3-signed-builder.js.map +1 -1
- package/dist/auth/web3-signed.cjs +11 -0
- package/dist/auth/web3-signed.cjs.map +1 -1
- package/dist/auth/web3-signed.d.ts +7 -0
- package/dist/auth/web3-signed.js +10 -0
- package/dist/auth/web3-signed.js.map +1 -1
- package/dist/index.browser.js +9 -0
- package/dist/index.browser.js.map +2 -2
- package/dist/index.node.cjs +9 -0
- package/dist/index.node.cjs.map +2 -2
- package/dist/index.node.js +9 -0
- package/dist/index.node.js.map +2 -2
- package/dist/protocol/jobs-client.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
|
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":"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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":[]}
|
|
@@ -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
|
*
|
package/dist/auth/web3-signed.js
CHANGED
|
@@ -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":[]}
|
package/dist/index.browser.js
CHANGED
|
@@ -31221,6 +31221,7 @@ import {
|
|
|
31221
31221
|
} from "./auth/errors.js";
|
|
31222
31222
|
var WEB3_SIGNED_PREFIX = "Web3Signed ";
|
|
31223
31223
|
var CLOCK_SKEW_SECONDS = 60;
|
|
31224
|
+
var MAX_WEB3SIGNED_LIFETIME_SECONDS = 15 * 60;
|
|
31224
31225
|
function base64urlDecode(input) {
|
|
31225
31226
|
let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
31226
31227
|
const padLength = (4 - base64.length % 4) % 4;
|
|
@@ -31333,6 +31334,14 @@ async function verifyWeb3Signed(params) {
|
|
|
31333
31334
|
if (payload.iat > now + CLOCK_SKEW_SECONDS) {
|
|
31334
31335
|
throw new ExpiredTokenError({ reason: "Token issued in the future" });
|
|
31335
31336
|
}
|
|
31337
|
+
const lifetime = payload.exp - payload.iat;
|
|
31338
|
+
if (lifetime <= 0 || lifetime > MAX_WEB3SIGNED_LIFETIME_SECONDS) {
|
|
31339
|
+
throw new ExpiredTokenError({
|
|
31340
|
+
reason: "Token lifetime exceeds the maximum",
|
|
31341
|
+
maxLifetimeSeconds: MAX_WEB3SIGNED_LIFETIME_SECONDS,
|
|
31342
|
+
lifetimeSeconds: lifetime
|
|
31343
|
+
});
|
|
31344
|
+
}
|
|
31336
31345
|
return { signer, payload };
|
|
31337
31346
|
}
|
|
31338
31347
|
|