@pafi-dev/core 0.24.0 → 0.25.0
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/index.cjs +2 -2
- package/dist/auth/index.d.cts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/{chunk-75JWR5SA.cjs → chunk-6DSK5VR2.cjs} +1 -1
- package/dist/{chunk-75JWR5SA.cjs.map → chunk-6DSK5VR2.cjs.map} +1 -1
- package/dist/{chunk-4EGXLYMM.js → chunk-IKLFFJJK.js} +1 -1
- package/dist/{chunk-4EGXLYMM.js.map → chunk-IKLFFJJK.js.map} +1 -1
- package/dist/index.cjs +160 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -214
- package/dist/index.d.ts +106 -214
- package/dist/index.js +162 -134
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/auth/index.cjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
var
|
|
16
|
+
var _chunk6DSK5VR2cjs = require('../chunk-6DSK5VR2.cjs');
|
|
17
17
|
require('../chunk-JEQ2X3Z6.cjs');
|
|
18
18
|
|
|
19
19
|
|
|
@@ -30,5 +30,5 @@ require('../chunk-JEQ2X3Z6.cjs');
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
exports.SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET =
|
|
33
|
+
exports.SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET = _chunk6DSK5VR2cjs.SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET; exports.SPONSOR_AUTH_DOMAIN_NAME = _chunk6DSK5VR2cjs.SPONSOR_AUTH_DOMAIN_NAME; exports.SPONSOR_AUTH_TYPES = _chunk6DSK5VR2cjs.SPONSOR_AUTH_TYPES; exports.buildAndSignSponsorAuth = _chunk6DSK5VR2cjs.buildAndSignSponsorAuth; exports.buildSponsorAuthDomain = _chunk6DSK5VR2cjs.buildSponsorAuthDomain; exports.buildSponsorAuthTypedData = _chunk6DSK5VR2cjs.buildSponsorAuthTypedData; exports.computeCallDataHash = _chunk6DSK5VR2cjs.computeCallDataHash; exports.createLoginMessage = _chunk6DSK5VR2cjs.createLoginMessage; exports.generateSponsorAuthNonce = _chunk6DSK5VR2cjs.generateSponsorAuthNonce; exports.getSponsorAuthDomainAnchor = _chunk6DSK5VR2cjs.getSponsorAuthDomainAnchor; exports.parseLoginMessage = _chunk6DSK5VR2cjs.parseLoginMessage; exports.signSponsorAuth = _chunk6DSK5VR2cjs.signSponsorAuth; exports.verifyLoginMessage = _chunk6DSK5VR2cjs.verifyLoginMessage; exports.verifySponsorAuth = _chunk6DSK5VR2cjs.verifySponsorAuth;
|
|
34
34
|
//# sourceMappingURL=index.cjs.map
|
package/dist/auth/index.d.cts
CHANGED
|
@@ -185,7 +185,7 @@ interface BuildSponsorAuthParams {
|
|
|
185
185
|
callData: Hex;
|
|
186
186
|
/** Chain id the UserOp will execute on. */
|
|
187
187
|
chainId: number;
|
|
188
|
-
/** Scenario tag for
|
|
188
|
+
/** Scenario tag for logs / rate-limiter (`mint` / `burn` / `swap` / etc.). */
|
|
189
189
|
scenario: string;
|
|
190
190
|
/** Issuer id (matches `pafi_issuers` row in PAFI's sponsor-relayer). */
|
|
191
191
|
issuerId: string;
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -185,7 +185,7 @@ interface BuildSponsorAuthParams {
|
|
|
185
185
|
callData: Hex;
|
|
186
186
|
/** Chain id the UserOp will execute on. */
|
|
187
187
|
chainId: number;
|
|
188
|
-
/** Scenario tag for
|
|
188
|
+
/** Scenario tag for logs / rate-limiter (`mint` / `burn` / `swap` / etc.). */
|
|
189
189
|
scenario: string;
|
|
190
190
|
/** Issuer id (matches `pafi_issuers` row in PAFI's sponsor-relayer). */
|
|
191
191
|
issuerId: string;
|
package/dist/auth/index.js
CHANGED
|
@@ -322,4 +322,4 @@ async function verifySponsorAuth(payload, signature, expectedSigner) {
|
|
|
322
322
|
|
|
323
323
|
|
|
324
324
|
exports.createLoginMessage = createLoginMessage; exports.parseLoginMessage = parseLoginMessage; exports.verifyLoginMessage = verifyLoginMessage; exports.generateSponsorAuthNonce = generateSponsorAuthNonce; exports.SPONSOR_AUTH_DOMAIN_NAME = SPONSOR_AUTH_DOMAIN_NAME; exports.SPONSOR_AUTH_TYPES = SPONSOR_AUTH_TYPES; exports.SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET = SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET; exports.getSponsorAuthDomainAnchor = getSponsorAuthDomainAnchor; exports.buildSponsorAuthDomain = buildSponsorAuthDomain; exports.buildSponsorAuthTypedData = buildSponsorAuthTypedData; exports.computeCallDataHash = computeCallDataHash; exports.signSponsorAuth = signSponsorAuth; exports.buildAndSignSponsorAuth = buildAndSignSponsorAuth; exports.verifySponsorAuth = verifySponsorAuth;
|
|
325
|
-
//# sourceMappingURL=chunk-
|
|
325
|
+
//# sourceMappingURL=chunk-6DSK5VR2.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/phitran/Pacific-Finance/pafi-backend/pafi-sdk/packages/core/dist/chunk-75JWR5SA.cjs","../src/auth/loginMessage.ts","../src/auth/sponsorAuth.ts"],"names":[],"mappings":"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,IAAA;AACR,MAAA;AAEF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEgB;AACP,EAAA;AACC,IAAA;AACG,IAAA;AACT,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAES;AACA,EAAA;AACI,IAAA;AACD,IAAA;AACR,IAAA;AACO,IAAA;AACP,IAAA;AACU,IAAA;AACA,IAAA;AACZ,EAAA;AACF;AAEgB;AACP,EAAA;AACG,IAAA;AACD,IAAA;AACP,IAAA;AACS,IAAA;AACX,EAAA;AACF;AAEgB;AACP,EAAA;AACT;AAEA;AAIU,EAAA;AAED,EAAA;AACI,IAAA;AACT,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACD,EAAA;AACH;AA4DA;AAGQ,EAAA;AAGA,EAAA;AAEA,EAAA;AACK,IAAA;AACD,IAAA;AACR,IAAA;AACA,IAAA;AACA,IAAA;AACU,IAAA;AACA,IAAA;AACZ,EAAA;AAEY,EAAA;AAEL,EAAA;AACL,IAAA;AACS,IAAA;AACD,IAAA;AACR,IAAA;AACO,IAAA;AACP,IAAA;AACU,IAAA;AACA,IAAA;AACZ,EAAA;AACF;AAEA;AAKQ,EAAA;AACM,EAAA;AACD,IAAA;AACX,EAAA;AAQK,EAAA;AACM,IAAA;AACX,EAAA;AAEI,EAAA;AACA,EAAA;AACM,IAAA;AAER,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACK,EAAA;AACG,IAAA;AACX,EAAA;AAEI,EAAA;AACO,IAAA;AACX,EAAA;AAES,EAAA;AACX;AFEc;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/phitran/Pacific-Finance/pafi-backend/pafi-sdk/packages/core/dist/chunk-75JWR5SA.cjs","sourcesContent":[null,"import { getAddress, verifyMessage, recoverMessageAddress } from \"viem\";\nimport type { Address, Hex } from \"viem\";\nimport type { LoginMessageParams, VerifyLoginResult } from \"./types\";\n\nconst DEFAULT_VERSION = \"1\";\nconst DEFAULT_STATEMENT = \"Sign in with Ethereum to PAFI.\";\n\n/**\n * Build an EIP-4361 login message string.\n *\n * The output is a deterministic plain-text message that the wallet signs via\n * `personal_sign`. The same message can be parsed back with\n * {@link parseLoginMessage} and verified with {@link verifyLoginMessage}.\n */\nexport function createLoginMessage(params: LoginMessageParams): string {\n const {\n domain,\n address,\n chainId,\n nonce,\n uri,\n statement = DEFAULT_STATEMENT,\n version = DEFAULT_VERSION,\n issuedAt = new Date(),\n expirationTime,\n notBefore,\n requestId,\n } = params;\n\n if (!domain) throw new Error(\"createLoginMessage: domain required\");\n if (!nonce) throw new Error(\"createLoginMessage: nonce required\");\n if (!uri) throw new Error(\"createLoginMessage: uri required\");\n\n const checksummed = getAddress(address);\n\n const lines: string[] = [];\n lines.push(`${domain} wants you to sign in with your Ethereum account:`);\n lines.push(checksummed);\n lines.push(\"\");\n if (statement) {\n lines.push(statement);\n lines.push(\"\");\n }\n lines.push(`URI: ${uri}`);\n lines.push(`Version: ${version}`);\n lines.push(`Chain ID: ${chainId}`);\n lines.push(`Nonce: ${nonce}`);\n lines.push(`Issued At: ${issuedAt.toISOString()}`);\n if (expirationTime) {\n lines.push(`Expiration Time: ${expirationTime.toISOString()}`);\n }\n if (notBefore) {\n lines.push(`Not Before: ${notBefore.toISOString()}`);\n }\n if (requestId) {\n lines.push(`Request ID: ${requestId}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Parse a login message string built by {@link createLoginMessage} back into\n * its structured fields. Throws if the message does not match the expected\n * EIP-4361 layout.\n */\nexport function parseLoginMessage(message: string): LoginMessageParams {\n const lines = message.split(\"\\n\");\n if (lines.length < 7) {\n throw new Error(\"parseLoginMessage: message too short\");\n }\n\n const headerLine = lines[0] ?? \"\";\n const headerMatch = headerLine.match(\n /^(?<domain>.+) wants you to sign in with your Ethereum account:$/,\n );\n if (!headerMatch || !headerMatch.groups) {\n throw new Error(\"parseLoginMessage: invalid header line\");\n }\n const domain = headerMatch.groups[\"domain\"]!;\n\n const addressLine = lines[1] ?? \"\";\n if (!/^0x[0-9a-fA-F]{40}$/.test(addressLine)) {\n throw new Error(\"parseLoginMessage: invalid address line\");\n }\n const address = getAddress(addressLine);\n\n // After address: blank line, optional statement + blank line, then key:value lines.\n let cursor = 2;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after address\");\n }\n cursor++;\n\n let statement: string | undefined;\n // Statement is present if the next line is not a known key.\n if (cursor < lines.length && !looksLikeKeyValue(lines[cursor]!)) {\n statement = lines[cursor];\n cursor++;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after statement\");\n }\n cursor++;\n }\n\n const fields = new Map<string, string>();\n for (; cursor < lines.length; cursor++) {\n const line = lines[cursor]!;\n if (line === \"\") continue;\n const idx = line.indexOf(\": \");\n if (idx === -1) {\n throw new Error(`parseLoginMessage: malformed field line: \"${line}\"`);\n }\n const key = line.slice(0, idx);\n const value = line.slice(idx + 2);\n fields.set(key, value);\n }\n\n const uri = requireField(fields, \"URI\");\n const version = requireField(fields, \"Version\");\n const chainId = Number(requireField(fields, \"Chain ID\"));\n if (!Number.isInteger(chainId)) {\n throw new Error(\"parseLoginMessage: Chain ID is not an integer\");\n }\n const nonce = requireField(fields, \"Nonce\");\n const issuedAt = new Date(requireField(fields, \"Issued At\"));\n const expirationTime = optionalDate(fields, \"Expiration Time\");\n const notBefore = optionalDate(fields, \"Not Before\");\n const requestId = fields.get(\"Request ID\");\n\n const result: LoginMessageParams = {\n domain,\n address,\n chainId,\n nonce,\n uri,\n version,\n issuedAt,\n };\n if (statement !== undefined) result.statement = statement;\n if (expirationTime !== undefined) result.expirationTime = expirationTime;\n if (notBefore !== undefined) result.notBefore = notBefore;\n if (requestId !== undefined) result.requestId = requestId;\n return result;\n}\n\n/**\n * Verify that a login message was signed by the address embedded in the\n * message. Returns `{ valid, address }` where `address` is the recovered\n * signer (checksummed). Does NOT check expiration / not-before / nonce\n * consumption — those are the AuthService's responsibility.\n */\nexport async function verifyLoginMessage(\n message: string,\n signature: Hex,\n): Promise<VerifyLoginResult> {\n const parsed = parseLoginMessage(message);\n const valid = await verifyMessage({\n address: parsed.address,\n message,\n signature,\n });\n if (valid) {\n return { valid: true, address: parsed.address };\n }\n // Recover anyway so callers can log the mismatch.\n const recovered = await recoverMessageAddress({ message, signature });\n return { valid: false, address: getAddress(recovered) as Address };\n}\n\n// -------------------------------------------------------------------------\n// helpers\n// -------------------------------------------------------------------------\n\nconst KNOWN_KEYS = new Set([\n \"URI\",\n \"Version\",\n \"Chain ID\",\n \"Nonce\",\n \"Issued At\",\n \"Expiration Time\",\n \"Not Before\",\n \"Request ID\",\n]);\n\nfunction looksLikeKeyValue(line: string): boolean {\n const idx = line.indexOf(\": \");\n if (idx === -1) return false;\n return KNOWN_KEYS.has(line.slice(0, idx));\n}\n\nfunction requireField(fields: Map<string, string>, key: string): string {\n const value = fields.get(key);\n if (value === undefined) {\n throw new Error(`parseLoginMessage: missing required field \"${key}\"`);\n }\n return value;\n}\n\nfunction optionalDate(\n fields: Map<string, string>,\n key: string,\n): Date | undefined {\n const raw = fields.get(key);\n if (raw === undefined) return undefined;\n return new Date(raw);\n}\n","import { keccak256, recoverTypedDataAddress } from \"viem\";\nimport type { Address, Hex, WalletClient } from \"viem\";\n\n/**\n * Generate a high-entropy nonce for SponsorAuth. Uses 64 bits of CSPRNG\n * (`crypto.randomBytes` Node, `crypto.getRandomValues` browser) in the low\n * half plus a millisecond timestamp prefix in the high half — keeps\n * sponsor-relayer's single-use nonce gate clean at scale.\n */\nexport function generateSponsorAuthNonce(): bigint {\n // Per-spec timestamp prefix (high 64 bits, ms precision) so server-side\n // freshness checks can derive an approx age from the nonce alone.\n const tsMs = BigInt(Date.now());\n\n // 64 bits of CSPRNG entropy in the low half. Try Web Crypto first (browser\n // + modern Node), fall back to node:crypto.\n let randHigh: number;\n let randLow: number;\n const g = globalThis as unknown as {\n crypto?: { getRandomValues?: (a: Uint32Array) => Uint32Array };\n };\n if (g.crypto?.getRandomValues) {\n const buf = new Uint32Array(2);\n g.crypto.getRandomValues(buf);\n randHigh = buf[0]!;\n randLow = buf[1]!;\n } else {\n // Node fallback — require lazily so browser bundlers can tree-shake.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const nodeCrypto = require(\"node:crypto\") as {\n randomBytes: (n: number) => Buffer;\n };\n const b = nodeCrypto.randomBytes(8);\n randHigh = b.readUInt32BE(0);\n randLow = b.readUInt32BE(4);\n }\n\n const rand =\n (BigInt(randHigh) << 32n) | BigInt(randLow);\n return (tsMs << 64n) | rand;\n}\n\n/**\n * Validate hex-encoded signature shape: starts with `0x`, even length\n * AFTER the prefix, and 65 bytes (`0x` + 130 hex) minimum. Allows\n * larger sizes for EIP-1271 contract signatures.\n */\nfunction isValidHexSignature(sig: string): boolean {\n if (!sig.startsWith(\"0x\")) return false;\n const hex = sig.slice(2);\n // Must be even length (each byte = 2 hex chars).\n if (hex.length % 2 !== 0) return false;\n // ECDSA = 65 bytes = 130 hex chars. EIP-1271 may be longer; reject\n // shorter lengths.\n if (hex.length < 130) return false;\n // Hex regex (0–9, a–f, A–F).\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\nexport const SPONSOR_AUTH_DOMAIN_NAME = \"PafiSponsorAuth\";\n\nexport const SPONSOR_AUTH_TYPES = {\n SponsorAuth: [\n { name: \"chainId\", type: \"uint256\" },\n { name: \"sender\", type: \"address\" },\n { name: \"callDataHash\", type: \"bytes32\" },\n { name: \"nonce\", type: \"uint256\" },\n { name: \"expiresAt\", type: \"uint256\" },\n { name: \"scenario\", type: \"string\" },\n { name: \"issuerId\", type: \"string\" },\n ],\n} as const;\n\nexport interface SponsorAuthPayload {\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n nonce: bigint;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface SponsorAuthVerifyResult {\n ok: boolean;\n recoveredAddress?: Address;\n reason?: \"EXPIRED\" | \"INVALID_SIGNER\" | \"INVALID_SIGNATURE_FORMAT\";\n}\n\n/**\n * Domain anchor for SponsorAuth EIP-712 signatures. The relayer service\n * is off-chain (no on-chain SponsorRelayer contract), so we bind to a\n * stable PAFI-controlled marker registered as `SPONSOR_AUTH_DOMAIN_ANCHOR`\n * in the relayer config. This rules out cross-system replay (a sibling\n * EIP-712 payload with the same field names cannot validate against\n * this domain) and gives wallets a meaningful \"verifying contract\"\n * preview instead of `0x0`.\n *\n * If you ever deploy an on-chain `SponsorRelayer` contract, switch this\n * to its address.\n */\n// Bytes 1-4 = \"PAFI\" (0x50414649); bytes 5-12 = \"SPONSOR_\" (0x53504F4E534F525F);\n// bytes 13-19 = padding; byte 20 = version tag (0x01). Total = 20 bytes.\n// Stable, system-specific marker. To migrate to a real EOA / on-chain\n// SponsorRelayer contract, replace this constant and bump `version` to \"2\".\nexport const SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET: Address =\n \"0x5041464953504F4E534F525F0000000000000001\" as Address;\n\nconst SPONSOR_AUTH_DOMAIN_ANCHORS: Record<number, Address> = {\n // Base mainnet — uses the literal-tag anchor above. To migrate to a\n // real EOA / contract, replace this entry and bump `version` to \"2\"\n // so existing sigs become invalid.\n 8453: SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET,\n};\n\nexport function getSponsorAuthDomainAnchor(chainId: number): Address {\n const anchor = SPONSOR_AUTH_DOMAIN_ANCHORS[chainId];\n if (!anchor) {\n throw new Error(\n `buildSponsorAuthDomain: no SponsorAuth domain anchor configured for chainId ${chainId}. ` +\n `Add an entry to SPONSOR_AUTH_DOMAIN_ANCHORS in @pafi-dev/core/auth/sponsorAuth.ts.`,\n );\n }\n return anchor;\n}\n\nexport function buildSponsorAuthDomain(chainId: number) {\n return {\n name: SPONSOR_AUTH_DOMAIN_NAME,\n version: \"1\",\n chainId,\n verifyingContract: getSponsorAuthDomainAnchor(chainId),\n };\n}\n\nfunction buildMessage(payload: SponsorAuthPayload) {\n return {\n chainId: BigInt(payload.chainId),\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce,\n expiresAt: BigInt(payload.expiresAt),\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport function buildSponsorAuthTypedData(payload: SponsorAuthPayload) {\n return {\n domain: buildSponsorAuthDomain(payload.chainId),\n types: SPONSOR_AUTH_TYPES,\n primaryType: \"SponsorAuth\" as const,\n message: buildMessage(payload),\n };\n}\n\nexport function computeCallDataHash(callData: Hex): Hex {\n return keccak256(callData);\n}\n\nexport async function signSponsorAuth(\n wallet: WalletClient,\n payload: SponsorAuthPayload,\n): Promise<Hex> {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n return wallet.signTypedData({\n account: wallet.account!,\n domain,\n types,\n primaryType,\n message,\n });\n}\n\nexport interface BuiltSponsorAuth {\n sig: Hex;\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n /** Decimal-string for JSON transport (bigint not safely serializable). */\n nonce: string;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface BuildSponsorAuthParams {\n /** User EOA the sponsorship is for. */\n userAddress: Address;\n /** UserOp `callData` to bind the auth to (hashed via keccak256). */\n callData: Hex;\n /** Chain id the UserOp will execute on. */\n chainId: number;\n /** Scenario tag for audit / rate-limiter (`mint` / `burn` / `swap` / etc.). */\n scenario: string;\n /** Issuer id (matches `pafi_issuers` row in PAFI's sponsor-relayer). */\n issuerId: string;\n /** Issuer signer wallet (HSM/KMS-backed in production). */\n issuerSignerWallet: WalletClient;\n /** Validity window in seconds. Default 600 (10 min). */\n expiresInSeconds?: number;\n /**\n * Optional explicit nonce. When omitted, defaults to a unique\n * `Date.now() * 1e6 + random` — matches the gg56 reference impl.\n * Override for test fixtures or when chaining replay-protection.\n */\n nonce?: bigint;\n}\n\n/**\n * Build, sign, and serialize a `SponsorAuth` payload in one call.\n * Replaces the ~30 LoC private `buildSponsorAuth` helper that every\n * issuer would otherwise reimplement on each sponsored endpoint.\n *\n * Output is JSON-safe (`nonce` as decimal string) and matches the\n * shape sponsor-relayer's `/paymaster/sponsor` accepts as the\n * `sponsorAuth` field.\n *\n * Issuer typically calls this from each sponsored controller route:\n *\n * ```ts\n * const sponsorAuth = await buildAndSignSponsorAuth({\n * userAddress: user.userAddress,\n * callData: userOp.callData,\n * chainId,\n * scenario: 'mint',\n * issuerId: this.config.get('PAFI_ISSUER_ID'),\n * issuerSignerWallet: this.issuerSignerWallet,\n * });\n * return { ...response, sponsorAuth };\n * ```\n */\nexport async function buildAndSignSponsorAuth(\n params: BuildSponsorAuthParams,\n): Promise<BuiltSponsorAuth> {\n const expiresAt =\n Math.floor(Date.now() / 1000) + (params.expiresInSeconds ?? 600);\n\n const nonce = params.nonce ?? generateSponsorAuthNonce();\n\n const payload: SponsorAuthPayload = {\n chainId: params.chainId,\n sender: params.userAddress,\n callDataHash: computeCallDataHash(params.callData),\n nonce,\n expiresAt,\n scenario: params.scenario,\n issuerId: params.issuerId,\n };\n\n const sig = await signSponsorAuth(params.issuerSignerWallet, payload);\n\n return {\n sig,\n chainId: payload.chainId,\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce.toString(),\n expiresAt: payload.expiresAt,\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport async function verifySponsorAuth(\n payload: SponsorAuthPayload,\n signature: Hex,\n expectedSigner: Address,\n): Promise<SponsorAuthVerifyResult> {\n const nowSec = Math.floor(Date.now() / 1000);\n if (payload.expiresAt < nowSec) {\n return { ok: false, reason: \"EXPIRED\" };\n }\n\n // v0.7.1 — strict hex format check before deferring to viem. Previous\n // length-only `< 132` accepted arbitrary 132+ char strings even when\n // not valid hex; recoverTypedDataAddress would throw and the catch\n // collapsed both cases to INVALID_SIGNATURE_FORMAT. Verify hex shape\n // up front + accept ECDSA (65 byte = 132 hex char) AND EIP-1271\n // (variable-length, must still be valid hex).\n if (!isValidHexSignature(signature)) {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n let recovered: Address;\n try {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n recovered = await recoverTypedDataAddress({\n domain,\n types,\n primaryType,\n message,\n signature,\n });\n } catch {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n if (recovered.toLowerCase() !== expectedSigner.toLowerCase()) {\n return { ok: false, reason: \"INVALID_SIGNER\", recoveredAddress: recovered };\n }\n\n return { ok: true, recoveredAddress: recovered };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/phitran/Pacific-Finance/pafi-backend/pafi-sdk/packages/core/dist/chunk-6DSK5VR2.cjs","../src/auth/loginMessage.ts","../src/auth/sponsorAuth.ts"],"names":[],"mappings":"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,IAAA;AACR,MAAA;AAEF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEgB;AACP,EAAA;AACC,IAAA;AACG,IAAA;AACT,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAES;AACA,EAAA;AACI,IAAA;AACD,IAAA;AACR,IAAA;AACO,IAAA;AACP,IAAA;AACU,IAAA;AACA,IAAA;AACZ,EAAA;AACF;AAEgB;AACP,EAAA;AACG,IAAA;AACD,IAAA;AACP,IAAA;AACS,IAAA;AACX,EAAA;AACF;AAEgB;AACP,EAAA;AACT;AAEA;AAIU,EAAA;AAED,EAAA;AACI,IAAA;AACT,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACD,EAAA;AACH;AA4DA;AAGQ,EAAA;AAGA,EAAA;AAEA,EAAA;AACK,IAAA;AACD,IAAA;AACR,IAAA;AACA,IAAA;AACA,IAAA;AACU,IAAA;AACA,IAAA;AACZ,EAAA;AAEY,EAAA;AAEL,EAAA;AACL,IAAA;AACS,IAAA;AACD,IAAA;AACR,IAAA;AACO,IAAA;AACP,IAAA;AACU,IAAA;AACA,IAAA;AACZ,EAAA;AACF;AAEA;AAKQ,EAAA;AACM,EAAA;AACD,IAAA;AACX,EAAA;AAQK,EAAA;AACM,IAAA;AACX,EAAA;AAEI,EAAA;AACA,EAAA;AACM,IAAA;AAER,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACK,EAAA;AACG,IAAA;AACX,EAAA;AAEI,EAAA;AACO,IAAA;AACX,EAAA;AAES,EAAA;AACX;AFEc;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/phitran/Pacific-Finance/pafi-backend/pafi-sdk/packages/core/dist/chunk-6DSK5VR2.cjs","sourcesContent":[null,"import { getAddress, verifyMessage, recoverMessageAddress } from \"viem\";\nimport type { Address, Hex } from \"viem\";\nimport type { LoginMessageParams, VerifyLoginResult } from \"./types\";\n\nconst DEFAULT_VERSION = \"1\";\nconst DEFAULT_STATEMENT = \"Sign in with Ethereum to PAFI.\";\n\n/**\n * Build an EIP-4361 login message string.\n *\n * The output is a deterministic plain-text message that the wallet signs via\n * `personal_sign`. The same message can be parsed back with\n * {@link parseLoginMessage} and verified with {@link verifyLoginMessage}.\n */\nexport function createLoginMessage(params: LoginMessageParams): string {\n const {\n domain,\n address,\n chainId,\n nonce,\n uri,\n statement = DEFAULT_STATEMENT,\n version = DEFAULT_VERSION,\n issuedAt = new Date(),\n expirationTime,\n notBefore,\n requestId,\n } = params;\n\n if (!domain) throw new Error(\"createLoginMessage: domain required\");\n if (!nonce) throw new Error(\"createLoginMessage: nonce required\");\n if (!uri) throw new Error(\"createLoginMessage: uri required\");\n\n const checksummed = getAddress(address);\n\n const lines: string[] = [];\n lines.push(`${domain} wants you to sign in with your Ethereum account:`);\n lines.push(checksummed);\n lines.push(\"\");\n if (statement) {\n lines.push(statement);\n lines.push(\"\");\n }\n lines.push(`URI: ${uri}`);\n lines.push(`Version: ${version}`);\n lines.push(`Chain ID: ${chainId}`);\n lines.push(`Nonce: ${nonce}`);\n lines.push(`Issued At: ${issuedAt.toISOString()}`);\n if (expirationTime) {\n lines.push(`Expiration Time: ${expirationTime.toISOString()}`);\n }\n if (notBefore) {\n lines.push(`Not Before: ${notBefore.toISOString()}`);\n }\n if (requestId) {\n lines.push(`Request ID: ${requestId}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Parse a login message string built by {@link createLoginMessage} back into\n * its structured fields. Throws if the message does not match the expected\n * EIP-4361 layout.\n */\nexport function parseLoginMessage(message: string): LoginMessageParams {\n const lines = message.split(\"\\n\");\n if (lines.length < 7) {\n throw new Error(\"parseLoginMessage: message too short\");\n }\n\n const headerLine = lines[0] ?? \"\";\n const headerMatch = headerLine.match(\n /^(?<domain>.+) wants you to sign in with your Ethereum account:$/,\n );\n if (!headerMatch || !headerMatch.groups) {\n throw new Error(\"parseLoginMessage: invalid header line\");\n }\n const domain = headerMatch.groups[\"domain\"]!;\n\n const addressLine = lines[1] ?? \"\";\n if (!/^0x[0-9a-fA-F]{40}$/.test(addressLine)) {\n throw new Error(\"parseLoginMessage: invalid address line\");\n }\n const address = getAddress(addressLine);\n\n // After address: blank line, optional statement + blank line, then key:value lines.\n let cursor = 2;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after address\");\n }\n cursor++;\n\n let statement: string | undefined;\n // Statement is present if the next line is not a known key.\n if (cursor < lines.length && !looksLikeKeyValue(lines[cursor]!)) {\n statement = lines[cursor];\n cursor++;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after statement\");\n }\n cursor++;\n }\n\n const fields = new Map<string, string>();\n for (; cursor < lines.length; cursor++) {\n const line = lines[cursor]!;\n if (line === \"\") continue;\n const idx = line.indexOf(\": \");\n if (idx === -1) {\n throw new Error(`parseLoginMessage: malformed field line: \"${line}\"`);\n }\n const key = line.slice(0, idx);\n const value = line.slice(idx + 2);\n fields.set(key, value);\n }\n\n const uri = requireField(fields, \"URI\");\n const version = requireField(fields, \"Version\");\n const chainId = Number(requireField(fields, \"Chain ID\"));\n if (!Number.isInteger(chainId)) {\n throw new Error(\"parseLoginMessage: Chain ID is not an integer\");\n }\n const nonce = requireField(fields, \"Nonce\");\n const issuedAt = new Date(requireField(fields, \"Issued At\"));\n const expirationTime = optionalDate(fields, \"Expiration Time\");\n const notBefore = optionalDate(fields, \"Not Before\");\n const requestId = fields.get(\"Request ID\");\n\n const result: LoginMessageParams = {\n domain,\n address,\n chainId,\n nonce,\n uri,\n version,\n issuedAt,\n };\n if (statement !== undefined) result.statement = statement;\n if (expirationTime !== undefined) result.expirationTime = expirationTime;\n if (notBefore !== undefined) result.notBefore = notBefore;\n if (requestId !== undefined) result.requestId = requestId;\n return result;\n}\n\n/**\n * Verify that a login message was signed by the address embedded in the\n * message. Returns `{ valid, address }` where `address` is the recovered\n * signer (checksummed). Does NOT check expiration / not-before / nonce\n * consumption — those are the AuthService's responsibility.\n */\nexport async function verifyLoginMessage(\n message: string,\n signature: Hex,\n): Promise<VerifyLoginResult> {\n const parsed = parseLoginMessage(message);\n const valid = await verifyMessage({\n address: parsed.address,\n message,\n signature,\n });\n if (valid) {\n return { valid: true, address: parsed.address };\n }\n // Recover anyway so callers can log the mismatch.\n const recovered = await recoverMessageAddress({ message, signature });\n return { valid: false, address: getAddress(recovered) as Address };\n}\n\n// -------------------------------------------------------------------------\n// helpers\n// -------------------------------------------------------------------------\n\nconst KNOWN_KEYS = new Set([\n \"URI\",\n \"Version\",\n \"Chain ID\",\n \"Nonce\",\n \"Issued At\",\n \"Expiration Time\",\n \"Not Before\",\n \"Request ID\",\n]);\n\nfunction looksLikeKeyValue(line: string): boolean {\n const idx = line.indexOf(\": \");\n if (idx === -1) return false;\n return KNOWN_KEYS.has(line.slice(0, idx));\n}\n\nfunction requireField(fields: Map<string, string>, key: string): string {\n const value = fields.get(key);\n if (value === undefined) {\n throw new Error(`parseLoginMessage: missing required field \"${key}\"`);\n }\n return value;\n}\n\nfunction optionalDate(\n fields: Map<string, string>,\n key: string,\n): Date | undefined {\n const raw = fields.get(key);\n if (raw === undefined) return undefined;\n return new Date(raw);\n}\n","import { keccak256, recoverTypedDataAddress } from \"viem\";\nimport type { Address, Hex, WalletClient } from \"viem\";\n\n/**\n * Generate a high-entropy nonce for SponsorAuth. Uses 64 bits of CSPRNG\n * (`crypto.randomBytes` Node, `crypto.getRandomValues` browser) in the low\n * half plus a millisecond timestamp prefix in the high half — keeps\n * sponsor-relayer's single-use nonce gate clean at scale.\n */\nexport function generateSponsorAuthNonce(): bigint {\n // Per-spec timestamp prefix (high 64 bits, ms precision) so server-side\n // freshness checks can derive an approx age from the nonce alone.\n const tsMs = BigInt(Date.now());\n\n // 64 bits of CSPRNG entropy in the low half. Try Web Crypto first (browser\n // + modern Node), fall back to node:crypto.\n let randHigh: number;\n let randLow: number;\n const g = globalThis as unknown as {\n crypto?: { getRandomValues?: (a: Uint32Array) => Uint32Array };\n };\n if (g.crypto?.getRandomValues) {\n const buf = new Uint32Array(2);\n g.crypto.getRandomValues(buf);\n randHigh = buf[0]!;\n randLow = buf[1]!;\n } else {\n // Node fallback — require lazily so browser bundlers can tree-shake.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const nodeCrypto = require(\"node:crypto\") as {\n randomBytes: (n: number) => Buffer;\n };\n const b = nodeCrypto.randomBytes(8);\n randHigh = b.readUInt32BE(0);\n randLow = b.readUInt32BE(4);\n }\n\n const rand =\n (BigInt(randHigh) << 32n) | BigInt(randLow);\n return (tsMs << 64n) | rand;\n}\n\n/**\n * Validate hex-encoded signature shape: starts with `0x`, even length\n * AFTER the prefix, and 65 bytes (`0x` + 130 hex) minimum. Allows\n * larger sizes for EIP-1271 contract signatures.\n */\nfunction isValidHexSignature(sig: string): boolean {\n if (!sig.startsWith(\"0x\")) return false;\n const hex = sig.slice(2);\n // Must be even length (each byte = 2 hex chars).\n if (hex.length % 2 !== 0) return false;\n // ECDSA = 65 bytes = 130 hex chars. EIP-1271 may be longer; reject\n // shorter lengths.\n if (hex.length < 130) return false;\n // Hex regex (0–9, a–f, A–F).\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\nexport const SPONSOR_AUTH_DOMAIN_NAME = \"PafiSponsorAuth\";\n\nexport const SPONSOR_AUTH_TYPES = {\n SponsorAuth: [\n { name: \"chainId\", type: \"uint256\" },\n { name: \"sender\", type: \"address\" },\n { name: \"callDataHash\", type: \"bytes32\" },\n { name: \"nonce\", type: \"uint256\" },\n { name: \"expiresAt\", type: \"uint256\" },\n { name: \"scenario\", type: \"string\" },\n { name: \"issuerId\", type: \"string\" },\n ],\n} as const;\n\nexport interface SponsorAuthPayload {\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n nonce: bigint;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface SponsorAuthVerifyResult {\n ok: boolean;\n recoveredAddress?: Address;\n reason?: \"EXPIRED\" | \"INVALID_SIGNER\" | \"INVALID_SIGNATURE_FORMAT\";\n}\n\n/**\n * Domain anchor for SponsorAuth EIP-712 signatures. The relayer service\n * is off-chain (no on-chain SponsorRelayer contract), so we bind to a\n * stable PAFI-controlled marker registered as `SPONSOR_AUTH_DOMAIN_ANCHOR`\n * in the relayer config. This rules out cross-system replay (a sibling\n * EIP-712 payload with the same field names cannot validate against\n * this domain) and gives wallets a meaningful \"verifying contract\"\n * preview instead of `0x0`.\n *\n * If you ever deploy an on-chain `SponsorRelayer` contract, switch this\n * to its address.\n */\n// Bytes 1-4 = \"PAFI\" (0x50414649); bytes 5-12 = \"SPONSOR_\" (0x53504F4E534F525F);\n// bytes 13-19 = padding; byte 20 = version tag (0x01). Total = 20 bytes.\n// Stable, system-specific marker. To migrate to a real EOA / on-chain\n// SponsorRelayer contract, replace this constant and bump `version` to \"2\".\nexport const SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET: Address =\n \"0x5041464953504F4E534F525F0000000000000001\" as Address;\n\nconst SPONSOR_AUTH_DOMAIN_ANCHORS: Record<number, Address> = {\n // Base mainnet — uses the literal-tag anchor above. To migrate to a\n // real EOA / contract, replace this entry and bump `version` to \"2\"\n // so existing sigs become invalid.\n 8453: SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET,\n};\n\nexport function getSponsorAuthDomainAnchor(chainId: number): Address {\n const anchor = SPONSOR_AUTH_DOMAIN_ANCHORS[chainId];\n if (!anchor) {\n throw new Error(\n `buildSponsorAuthDomain: no SponsorAuth domain anchor configured for chainId ${chainId}. ` +\n `Add an entry to SPONSOR_AUTH_DOMAIN_ANCHORS in @pafi-dev/core/auth/sponsorAuth.ts.`,\n );\n }\n return anchor;\n}\n\nexport function buildSponsorAuthDomain(chainId: number) {\n return {\n name: SPONSOR_AUTH_DOMAIN_NAME,\n version: \"1\",\n chainId,\n verifyingContract: getSponsorAuthDomainAnchor(chainId),\n };\n}\n\nfunction buildMessage(payload: SponsorAuthPayload) {\n return {\n chainId: BigInt(payload.chainId),\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce,\n expiresAt: BigInt(payload.expiresAt),\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport function buildSponsorAuthTypedData(payload: SponsorAuthPayload) {\n return {\n domain: buildSponsorAuthDomain(payload.chainId),\n types: SPONSOR_AUTH_TYPES,\n primaryType: \"SponsorAuth\" as const,\n message: buildMessage(payload),\n };\n}\n\nexport function computeCallDataHash(callData: Hex): Hex {\n return keccak256(callData);\n}\n\nexport async function signSponsorAuth(\n wallet: WalletClient,\n payload: SponsorAuthPayload,\n): Promise<Hex> {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n return wallet.signTypedData({\n account: wallet.account!,\n domain,\n types,\n primaryType,\n message,\n });\n}\n\nexport interface BuiltSponsorAuth {\n sig: Hex;\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n /** Decimal-string for JSON transport (bigint not safely serializable). */\n nonce: string;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface BuildSponsorAuthParams {\n /** User EOA the sponsorship is for. */\n userAddress: Address;\n /** UserOp `callData` to bind the auth to (hashed via keccak256). */\n callData: Hex;\n /** Chain id the UserOp will execute on. */\n chainId: number;\n /** Scenario tag for logs / rate-limiter (`mint` / `burn` / `swap` / etc.). */\n scenario: string;\n /** Issuer id (matches `pafi_issuers` row in PAFI's sponsor-relayer). */\n issuerId: string;\n /** Issuer signer wallet (HSM/KMS-backed in production). */\n issuerSignerWallet: WalletClient;\n /** Validity window in seconds. Default 600 (10 min). */\n expiresInSeconds?: number;\n /**\n * Optional explicit nonce. When omitted, defaults to a unique\n * `Date.now() * 1e6 + random` — matches the gg56 reference impl.\n * Override for test fixtures or when chaining replay-protection.\n */\n nonce?: bigint;\n}\n\n/**\n * Build, sign, and serialize a `SponsorAuth` payload in one call.\n * Replaces the ~30 LoC private `buildSponsorAuth` helper that every\n * issuer would otherwise reimplement on each sponsored endpoint.\n *\n * Output is JSON-safe (`nonce` as decimal string) and matches the\n * shape sponsor-relayer's `/paymaster/sponsor` accepts as the\n * `sponsorAuth` field.\n *\n * Issuer typically calls this from each sponsored controller route:\n *\n * ```ts\n * const sponsorAuth = await buildAndSignSponsorAuth({\n * userAddress: user.userAddress,\n * callData: userOp.callData,\n * chainId,\n * scenario: 'mint',\n * issuerId: this.config.get('PAFI_ISSUER_ID'),\n * issuerSignerWallet: this.issuerSignerWallet,\n * });\n * return { ...response, sponsorAuth };\n * ```\n */\nexport async function buildAndSignSponsorAuth(\n params: BuildSponsorAuthParams,\n): Promise<BuiltSponsorAuth> {\n const expiresAt =\n Math.floor(Date.now() / 1000) + (params.expiresInSeconds ?? 600);\n\n const nonce = params.nonce ?? generateSponsorAuthNonce();\n\n const payload: SponsorAuthPayload = {\n chainId: params.chainId,\n sender: params.userAddress,\n callDataHash: computeCallDataHash(params.callData),\n nonce,\n expiresAt,\n scenario: params.scenario,\n issuerId: params.issuerId,\n };\n\n const sig = await signSponsorAuth(params.issuerSignerWallet, payload);\n\n return {\n sig,\n chainId: payload.chainId,\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce.toString(),\n expiresAt: payload.expiresAt,\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport async function verifySponsorAuth(\n payload: SponsorAuthPayload,\n signature: Hex,\n expectedSigner: Address,\n): Promise<SponsorAuthVerifyResult> {\n const nowSec = Math.floor(Date.now() / 1000);\n if (payload.expiresAt < nowSec) {\n return { ok: false, reason: \"EXPIRED\" };\n }\n\n // v0.7.1 — strict hex format check before deferring to viem. Previous\n // length-only `< 132` accepted arbitrary 132+ char strings even when\n // not valid hex; recoverTypedDataAddress would throw and the catch\n // collapsed both cases to INVALID_SIGNATURE_FORMAT. Verify hex shape\n // up front + accept ECDSA (65 byte = 132 hex char) AND EIP-1271\n // (variable-length, must still be valid hex).\n if (!isValidHexSignature(signature)) {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n let recovered: Address;\n try {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n recovered = await recoverTypedDataAddress({\n domain,\n types,\n primaryType,\n message,\n signature,\n });\n } catch {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n if (recovered.toLowerCase() !== expectedSigner.toLowerCase()) {\n return { ok: false, reason: \"INVALID_SIGNER\", recoveredAddress: recovered };\n }\n\n return { ok: true, recoveredAddress: recovered };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/loginMessage.ts","../src/auth/sponsorAuth.ts"],"sourcesContent":["import { getAddress, verifyMessage, recoverMessageAddress } from \"viem\";\nimport type { Address, Hex } from \"viem\";\nimport type { LoginMessageParams, VerifyLoginResult } from \"./types\";\n\nconst DEFAULT_VERSION = \"1\";\nconst DEFAULT_STATEMENT = \"Sign in with Ethereum to PAFI.\";\n\n/**\n * Build an EIP-4361 login message string.\n *\n * The output is a deterministic plain-text message that the wallet signs via\n * `personal_sign`. The same message can be parsed back with\n * {@link parseLoginMessage} and verified with {@link verifyLoginMessage}.\n */\nexport function createLoginMessage(params: LoginMessageParams): string {\n const {\n domain,\n address,\n chainId,\n nonce,\n uri,\n statement = DEFAULT_STATEMENT,\n version = DEFAULT_VERSION,\n issuedAt = new Date(),\n expirationTime,\n notBefore,\n requestId,\n } = params;\n\n if (!domain) throw new Error(\"createLoginMessage: domain required\");\n if (!nonce) throw new Error(\"createLoginMessage: nonce required\");\n if (!uri) throw new Error(\"createLoginMessage: uri required\");\n\n const checksummed = getAddress(address);\n\n const lines: string[] = [];\n lines.push(`${domain} wants you to sign in with your Ethereum account:`);\n lines.push(checksummed);\n lines.push(\"\");\n if (statement) {\n lines.push(statement);\n lines.push(\"\");\n }\n lines.push(`URI: ${uri}`);\n lines.push(`Version: ${version}`);\n lines.push(`Chain ID: ${chainId}`);\n lines.push(`Nonce: ${nonce}`);\n lines.push(`Issued At: ${issuedAt.toISOString()}`);\n if (expirationTime) {\n lines.push(`Expiration Time: ${expirationTime.toISOString()}`);\n }\n if (notBefore) {\n lines.push(`Not Before: ${notBefore.toISOString()}`);\n }\n if (requestId) {\n lines.push(`Request ID: ${requestId}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Parse a login message string built by {@link createLoginMessage} back into\n * its structured fields. Throws if the message does not match the expected\n * EIP-4361 layout.\n */\nexport function parseLoginMessage(message: string): LoginMessageParams {\n const lines = message.split(\"\\n\");\n if (lines.length < 7) {\n throw new Error(\"parseLoginMessage: message too short\");\n }\n\n const headerLine = lines[0] ?? \"\";\n const headerMatch = headerLine.match(\n /^(?<domain>.+) wants you to sign in with your Ethereum account:$/,\n );\n if (!headerMatch || !headerMatch.groups) {\n throw new Error(\"parseLoginMessage: invalid header line\");\n }\n const domain = headerMatch.groups[\"domain\"]!;\n\n const addressLine = lines[1] ?? \"\";\n if (!/^0x[0-9a-fA-F]{40}$/.test(addressLine)) {\n throw new Error(\"parseLoginMessage: invalid address line\");\n }\n const address = getAddress(addressLine);\n\n // After address: blank line, optional statement + blank line, then key:value lines.\n let cursor = 2;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after address\");\n }\n cursor++;\n\n let statement: string | undefined;\n // Statement is present if the next line is not a known key.\n if (cursor < lines.length && !looksLikeKeyValue(lines[cursor]!)) {\n statement = lines[cursor];\n cursor++;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after statement\");\n }\n cursor++;\n }\n\n const fields = new Map<string, string>();\n for (; cursor < lines.length; cursor++) {\n const line = lines[cursor]!;\n if (line === \"\") continue;\n const idx = line.indexOf(\": \");\n if (idx === -1) {\n throw new Error(`parseLoginMessage: malformed field line: \"${line}\"`);\n }\n const key = line.slice(0, idx);\n const value = line.slice(idx + 2);\n fields.set(key, value);\n }\n\n const uri = requireField(fields, \"URI\");\n const version = requireField(fields, \"Version\");\n const chainId = Number(requireField(fields, \"Chain ID\"));\n if (!Number.isInteger(chainId)) {\n throw new Error(\"parseLoginMessage: Chain ID is not an integer\");\n }\n const nonce = requireField(fields, \"Nonce\");\n const issuedAt = new Date(requireField(fields, \"Issued At\"));\n const expirationTime = optionalDate(fields, \"Expiration Time\");\n const notBefore = optionalDate(fields, \"Not Before\");\n const requestId = fields.get(\"Request ID\");\n\n const result: LoginMessageParams = {\n domain,\n address,\n chainId,\n nonce,\n uri,\n version,\n issuedAt,\n };\n if (statement !== undefined) result.statement = statement;\n if (expirationTime !== undefined) result.expirationTime = expirationTime;\n if (notBefore !== undefined) result.notBefore = notBefore;\n if (requestId !== undefined) result.requestId = requestId;\n return result;\n}\n\n/**\n * Verify that a login message was signed by the address embedded in the\n * message. Returns `{ valid, address }` where `address` is the recovered\n * signer (checksummed). Does NOT check expiration / not-before / nonce\n * consumption — those are the AuthService's responsibility.\n */\nexport async function verifyLoginMessage(\n message: string,\n signature: Hex,\n): Promise<VerifyLoginResult> {\n const parsed = parseLoginMessage(message);\n const valid = await verifyMessage({\n address: parsed.address,\n message,\n signature,\n });\n if (valid) {\n return { valid: true, address: parsed.address };\n }\n // Recover anyway so callers can log the mismatch.\n const recovered = await recoverMessageAddress({ message, signature });\n return { valid: false, address: getAddress(recovered) as Address };\n}\n\n// -------------------------------------------------------------------------\n// helpers\n// -------------------------------------------------------------------------\n\nconst KNOWN_KEYS = new Set([\n \"URI\",\n \"Version\",\n \"Chain ID\",\n \"Nonce\",\n \"Issued At\",\n \"Expiration Time\",\n \"Not Before\",\n \"Request ID\",\n]);\n\nfunction looksLikeKeyValue(line: string): boolean {\n const idx = line.indexOf(\": \");\n if (idx === -1) return false;\n return KNOWN_KEYS.has(line.slice(0, idx));\n}\n\nfunction requireField(fields: Map<string, string>, key: string): string {\n const value = fields.get(key);\n if (value === undefined) {\n throw new Error(`parseLoginMessage: missing required field \"${key}\"`);\n }\n return value;\n}\n\nfunction optionalDate(\n fields: Map<string, string>,\n key: string,\n): Date | undefined {\n const raw = fields.get(key);\n if (raw === undefined) return undefined;\n return new Date(raw);\n}\n","import { keccak256, recoverTypedDataAddress } from \"viem\";\nimport type { Address, Hex, WalletClient } from \"viem\";\n\n/**\n * Generate a high-entropy nonce for SponsorAuth. Uses 64 bits of CSPRNG\n * (`crypto.randomBytes` Node, `crypto.getRandomValues` browser) in the low\n * half plus a millisecond timestamp prefix in the high half — keeps\n * sponsor-relayer's single-use nonce gate clean at scale.\n */\nexport function generateSponsorAuthNonce(): bigint {\n // Per-spec timestamp prefix (high 64 bits, ms precision) so server-side\n // freshness checks can derive an approx age from the nonce alone.\n const tsMs = BigInt(Date.now());\n\n // 64 bits of CSPRNG entropy in the low half. Try Web Crypto first (browser\n // + modern Node), fall back to node:crypto.\n let randHigh: number;\n let randLow: number;\n const g = globalThis as unknown as {\n crypto?: { getRandomValues?: (a: Uint32Array) => Uint32Array };\n };\n if (g.crypto?.getRandomValues) {\n const buf = new Uint32Array(2);\n g.crypto.getRandomValues(buf);\n randHigh = buf[0]!;\n randLow = buf[1]!;\n } else {\n // Node fallback — require lazily so browser bundlers can tree-shake.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const nodeCrypto = require(\"node:crypto\") as {\n randomBytes: (n: number) => Buffer;\n };\n const b = nodeCrypto.randomBytes(8);\n randHigh = b.readUInt32BE(0);\n randLow = b.readUInt32BE(4);\n }\n\n const rand =\n (BigInt(randHigh) << 32n) | BigInt(randLow);\n return (tsMs << 64n) | rand;\n}\n\n/**\n * Validate hex-encoded signature shape: starts with `0x`, even length\n * AFTER the prefix, and 65 bytes (`0x` + 130 hex) minimum. Allows\n * larger sizes for EIP-1271 contract signatures.\n */\nfunction isValidHexSignature(sig: string): boolean {\n if (!sig.startsWith(\"0x\")) return false;\n const hex = sig.slice(2);\n // Must be even length (each byte = 2 hex chars).\n if (hex.length % 2 !== 0) return false;\n // ECDSA = 65 bytes = 130 hex chars. EIP-1271 may be longer; reject\n // shorter lengths.\n if (hex.length < 130) return false;\n // Hex regex (0–9, a–f, A–F).\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\nexport const SPONSOR_AUTH_DOMAIN_NAME = \"PafiSponsorAuth\";\n\nexport const SPONSOR_AUTH_TYPES = {\n SponsorAuth: [\n { name: \"chainId\", type: \"uint256\" },\n { name: \"sender\", type: \"address\" },\n { name: \"callDataHash\", type: \"bytes32\" },\n { name: \"nonce\", type: \"uint256\" },\n { name: \"expiresAt\", type: \"uint256\" },\n { name: \"scenario\", type: \"string\" },\n { name: \"issuerId\", type: \"string\" },\n ],\n} as const;\n\nexport interface SponsorAuthPayload {\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n nonce: bigint;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface SponsorAuthVerifyResult {\n ok: boolean;\n recoveredAddress?: Address;\n reason?: \"EXPIRED\" | \"INVALID_SIGNER\" | \"INVALID_SIGNATURE_FORMAT\";\n}\n\n/**\n * Domain anchor for SponsorAuth EIP-712 signatures. The relayer service\n * is off-chain (no on-chain SponsorRelayer contract), so we bind to a\n * stable PAFI-controlled marker registered as `SPONSOR_AUTH_DOMAIN_ANCHOR`\n * in the relayer config. This rules out cross-system replay (a sibling\n * EIP-712 payload with the same field names cannot validate against\n * this domain) and gives wallets a meaningful \"verifying contract\"\n * preview instead of `0x0`.\n *\n * If you ever deploy an on-chain `SponsorRelayer` contract, switch this\n * to its address.\n */\n// Bytes 1-4 = \"PAFI\" (0x50414649); bytes 5-12 = \"SPONSOR_\" (0x53504F4E534F525F);\n// bytes 13-19 = padding; byte 20 = version tag (0x01). Total = 20 bytes.\n// Stable, system-specific marker. To migrate to a real EOA / on-chain\n// SponsorRelayer contract, replace this constant and bump `version` to \"2\".\nexport const SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET: Address =\n \"0x5041464953504F4E534F525F0000000000000001\" as Address;\n\nconst SPONSOR_AUTH_DOMAIN_ANCHORS: Record<number, Address> = {\n // Base mainnet — uses the literal-tag anchor above. To migrate to a\n // real EOA / contract, replace this entry and bump `version` to \"2\"\n // so existing sigs become invalid.\n 8453: SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET,\n};\n\nexport function getSponsorAuthDomainAnchor(chainId: number): Address {\n const anchor = SPONSOR_AUTH_DOMAIN_ANCHORS[chainId];\n if (!anchor) {\n throw new Error(\n `buildSponsorAuthDomain: no SponsorAuth domain anchor configured for chainId ${chainId}. ` +\n `Add an entry to SPONSOR_AUTH_DOMAIN_ANCHORS in @pafi-dev/core/auth/sponsorAuth.ts.`,\n );\n }\n return anchor;\n}\n\nexport function buildSponsorAuthDomain(chainId: number) {\n return {\n name: SPONSOR_AUTH_DOMAIN_NAME,\n version: \"1\",\n chainId,\n verifyingContract: getSponsorAuthDomainAnchor(chainId),\n };\n}\n\nfunction buildMessage(payload: SponsorAuthPayload) {\n return {\n chainId: BigInt(payload.chainId),\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce,\n expiresAt: BigInt(payload.expiresAt),\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport function buildSponsorAuthTypedData(payload: SponsorAuthPayload) {\n return {\n domain: buildSponsorAuthDomain(payload.chainId),\n types: SPONSOR_AUTH_TYPES,\n primaryType: \"SponsorAuth\" as const,\n message: buildMessage(payload),\n };\n}\n\nexport function computeCallDataHash(callData: Hex): Hex {\n return keccak256(callData);\n}\n\nexport async function signSponsorAuth(\n wallet: WalletClient,\n payload: SponsorAuthPayload,\n): Promise<Hex> {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n return wallet.signTypedData({\n account: wallet.account!,\n domain,\n types,\n primaryType,\n message,\n });\n}\n\nexport interface BuiltSponsorAuth {\n sig: Hex;\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n /** Decimal-string for JSON transport (bigint not safely serializable). */\n nonce: string;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface BuildSponsorAuthParams {\n /** User EOA the sponsorship is for. */\n userAddress: Address;\n /** UserOp `callData` to bind the auth to (hashed via keccak256). */\n callData: Hex;\n /** Chain id the UserOp will execute on. */\n chainId: number;\n /** Scenario tag for audit / rate-limiter (`mint` / `burn` / `swap` / etc.). */\n scenario: string;\n /** Issuer id (matches `pafi_issuers` row in PAFI's sponsor-relayer). */\n issuerId: string;\n /** Issuer signer wallet (HSM/KMS-backed in production). */\n issuerSignerWallet: WalletClient;\n /** Validity window in seconds. Default 600 (10 min). */\n expiresInSeconds?: number;\n /**\n * Optional explicit nonce. When omitted, defaults to a unique\n * `Date.now() * 1e6 + random` — matches the gg56 reference impl.\n * Override for test fixtures or when chaining replay-protection.\n */\n nonce?: bigint;\n}\n\n/**\n * Build, sign, and serialize a `SponsorAuth` payload in one call.\n * Replaces the ~30 LoC private `buildSponsorAuth` helper that every\n * issuer would otherwise reimplement on each sponsored endpoint.\n *\n * Output is JSON-safe (`nonce` as decimal string) and matches the\n * shape sponsor-relayer's `/paymaster/sponsor` accepts as the\n * `sponsorAuth` field.\n *\n * Issuer typically calls this from each sponsored controller route:\n *\n * ```ts\n * const sponsorAuth = await buildAndSignSponsorAuth({\n * userAddress: user.userAddress,\n * callData: userOp.callData,\n * chainId,\n * scenario: 'mint',\n * issuerId: this.config.get('PAFI_ISSUER_ID'),\n * issuerSignerWallet: this.issuerSignerWallet,\n * });\n * return { ...response, sponsorAuth };\n * ```\n */\nexport async function buildAndSignSponsorAuth(\n params: BuildSponsorAuthParams,\n): Promise<BuiltSponsorAuth> {\n const expiresAt =\n Math.floor(Date.now() / 1000) + (params.expiresInSeconds ?? 600);\n\n const nonce = params.nonce ?? generateSponsorAuthNonce();\n\n const payload: SponsorAuthPayload = {\n chainId: params.chainId,\n sender: params.userAddress,\n callDataHash: computeCallDataHash(params.callData),\n nonce,\n expiresAt,\n scenario: params.scenario,\n issuerId: params.issuerId,\n };\n\n const sig = await signSponsorAuth(params.issuerSignerWallet, payload);\n\n return {\n sig,\n chainId: payload.chainId,\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce.toString(),\n expiresAt: payload.expiresAt,\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport async function verifySponsorAuth(\n payload: SponsorAuthPayload,\n signature: Hex,\n expectedSigner: Address,\n): Promise<SponsorAuthVerifyResult> {\n const nowSec = Math.floor(Date.now() / 1000);\n if (payload.expiresAt < nowSec) {\n return { ok: false, reason: \"EXPIRED\" };\n }\n\n // v0.7.1 — strict hex format check before deferring to viem. Previous\n // length-only `< 132` accepted arbitrary 132+ char strings even when\n // not valid hex; recoverTypedDataAddress would throw and the catch\n // collapsed both cases to INVALID_SIGNATURE_FORMAT. Verify hex shape\n // up front + accept ECDSA (65 byte = 132 hex char) AND EIP-1271\n // (variable-length, must still be valid hex).\n if (!isValidHexSignature(signature)) {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n let recovered: Address;\n try {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n recovered = await recoverTypedDataAddress({\n domain,\n types,\n primaryType,\n message,\n signature,\n });\n } catch {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n if (recovered.toLowerCase() !== expectedSigner.toLowerCase()) {\n return { ok: false, reason: \"INVALID_SIGNER\", recoveredAddress: recovered };\n }\n\n return { ok: true, recoveredAddress: recovered };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/auth/loginMessage.ts","../src/auth/sponsorAuth.ts"],"sourcesContent":["import { getAddress, verifyMessage, recoverMessageAddress } from \"viem\";\nimport type { Address, Hex } from \"viem\";\nimport type { LoginMessageParams, VerifyLoginResult } from \"./types\";\n\nconst DEFAULT_VERSION = \"1\";\nconst DEFAULT_STATEMENT = \"Sign in with Ethereum to PAFI.\";\n\n/**\n * Build an EIP-4361 login message string.\n *\n * The output is a deterministic plain-text message that the wallet signs via\n * `personal_sign`. The same message can be parsed back with\n * {@link parseLoginMessage} and verified with {@link verifyLoginMessage}.\n */\nexport function createLoginMessage(params: LoginMessageParams): string {\n const {\n domain,\n address,\n chainId,\n nonce,\n uri,\n statement = DEFAULT_STATEMENT,\n version = DEFAULT_VERSION,\n issuedAt = new Date(),\n expirationTime,\n notBefore,\n requestId,\n } = params;\n\n if (!domain) throw new Error(\"createLoginMessage: domain required\");\n if (!nonce) throw new Error(\"createLoginMessage: nonce required\");\n if (!uri) throw new Error(\"createLoginMessage: uri required\");\n\n const checksummed = getAddress(address);\n\n const lines: string[] = [];\n lines.push(`${domain} wants you to sign in with your Ethereum account:`);\n lines.push(checksummed);\n lines.push(\"\");\n if (statement) {\n lines.push(statement);\n lines.push(\"\");\n }\n lines.push(`URI: ${uri}`);\n lines.push(`Version: ${version}`);\n lines.push(`Chain ID: ${chainId}`);\n lines.push(`Nonce: ${nonce}`);\n lines.push(`Issued At: ${issuedAt.toISOString()}`);\n if (expirationTime) {\n lines.push(`Expiration Time: ${expirationTime.toISOString()}`);\n }\n if (notBefore) {\n lines.push(`Not Before: ${notBefore.toISOString()}`);\n }\n if (requestId) {\n lines.push(`Request ID: ${requestId}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Parse a login message string built by {@link createLoginMessage} back into\n * its structured fields. Throws if the message does not match the expected\n * EIP-4361 layout.\n */\nexport function parseLoginMessage(message: string): LoginMessageParams {\n const lines = message.split(\"\\n\");\n if (lines.length < 7) {\n throw new Error(\"parseLoginMessage: message too short\");\n }\n\n const headerLine = lines[0] ?? \"\";\n const headerMatch = headerLine.match(\n /^(?<domain>.+) wants you to sign in with your Ethereum account:$/,\n );\n if (!headerMatch || !headerMatch.groups) {\n throw new Error(\"parseLoginMessage: invalid header line\");\n }\n const domain = headerMatch.groups[\"domain\"]!;\n\n const addressLine = lines[1] ?? \"\";\n if (!/^0x[0-9a-fA-F]{40}$/.test(addressLine)) {\n throw new Error(\"parseLoginMessage: invalid address line\");\n }\n const address = getAddress(addressLine);\n\n // After address: blank line, optional statement + blank line, then key:value lines.\n let cursor = 2;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after address\");\n }\n cursor++;\n\n let statement: string | undefined;\n // Statement is present if the next line is not a known key.\n if (cursor < lines.length && !looksLikeKeyValue(lines[cursor]!)) {\n statement = lines[cursor];\n cursor++;\n if (lines[cursor] !== \"\") {\n throw new Error(\"parseLoginMessage: missing blank line after statement\");\n }\n cursor++;\n }\n\n const fields = new Map<string, string>();\n for (; cursor < lines.length; cursor++) {\n const line = lines[cursor]!;\n if (line === \"\") continue;\n const idx = line.indexOf(\": \");\n if (idx === -1) {\n throw new Error(`parseLoginMessage: malformed field line: \"${line}\"`);\n }\n const key = line.slice(0, idx);\n const value = line.slice(idx + 2);\n fields.set(key, value);\n }\n\n const uri = requireField(fields, \"URI\");\n const version = requireField(fields, \"Version\");\n const chainId = Number(requireField(fields, \"Chain ID\"));\n if (!Number.isInteger(chainId)) {\n throw new Error(\"parseLoginMessage: Chain ID is not an integer\");\n }\n const nonce = requireField(fields, \"Nonce\");\n const issuedAt = new Date(requireField(fields, \"Issued At\"));\n const expirationTime = optionalDate(fields, \"Expiration Time\");\n const notBefore = optionalDate(fields, \"Not Before\");\n const requestId = fields.get(\"Request ID\");\n\n const result: LoginMessageParams = {\n domain,\n address,\n chainId,\n nonce,\n uri,\n version,\n issuedAt,\n };\n if (statement !== undefined) result.statement = statement;\n if (expirationTime !== undefined) result.expirationTime = expirationTime;\n if (notBefore !== undefined) result.notBefore = notBefore;\n if (requestId !== undefined) result.requestId = requestId;\n return result;\n}\n\n/**\n * Verify that a login message was signed by the address embedded in the\n * message. Returns `{ valid, address }` where `address` is the recovered\n * signer (checksummed). Does NOT check expiration / not-before / nonce\n * consumption — those are the AuthService's responsibility.\n */\nexport async function verifyLoginMessage(\n message: string,\n signature: Hex,\n): Promise<VerifyLoginResult> {\n const parsed = parseLoginMessage(message);\n const valid = await verifyMessage({\n address: parsed.address,\n message,\n signature,\n });\n if (valid) {\n return { valid: true, address: parsed.address };\n }\n // Recover anyway so callers can log the mismatch.\n const recovered = await recoverMessageAddress({ message, signature });\n return { valid: false, address: getAddress(recovered) as Address };\n}\n\n// -------------------------------------------------------------------------\n// helpers\n// -------------------------------------------------------------------------\n\nconst KNOWN_KEYS = new Set([\n \"URI\",\n \"Version\",\n \"Chain ID\",\n \"Nonce\",\n \"Issued At\",\n \"Expiration Time\",\n \"Not Before\",\n \"Request ID\",\n]);\n\nfunction looksLikeKeyValue(line: string): boolean {\n const idx = line.indexOf(\": \");\n if (idx === -1) return false;\n return KNOWN_KEYS.has(line.slice(0, idx));\n}\n\nfunction requireField(fields: Map<string, string>, key: string): string {\n const value = fields.get(key);\n if (value === undefined) {\n throw new Error(`parseLoginMessage: missing required field \"${key}\"`);\n }\n return value;\n}\n\nfunction optionalDate(\n fields: Map<string, string>,\n key: string,\n): Date | undefined {\n const raw = fields.get(key);\n if (raw === undefined) return undefined;\n return new Date(raw);\n}\n","import { keccak256, recoverTypedDataAddress } from \"viem\";\nimport type { Address, Hex, WalletClient } from \"viem\";\n\n/**\n * Generate a high-entropy nonce for SponsorAuth. Uses 64 bits of CSPRNG\n * (`crypto.randomBytes` Node, `crypto.getRandomValues` browser) in the low\n * half plus a millisecond timestamp prefix in the high half — keeps\n * sponsor-relayer's single-use nonce gate clean at scale.\n */\nexport function generateSponsorAuthNonce(): bigint {\n // Per-spec timestamp prefix (high 64 bits, ms precision) so server-side\n // freshness checks can derive an approx age from the nonce alone.\n const tsMs = BigInt(Date.now());\n\n // 64 bits of CSPRNG entropy in the low half. Try Web Crypto first (browser\n // + modern Node), fall back to node:crypto.\n let randHigh: number;\n let randLow: number;\n const g = globalThis as unknown as {\n crypto?: { getRandomValues?: (a: Uint32Array) => Uint32Array };\n };\n if (g.crypto?.getRandomValues) {\n const buf = new Uint32Array(2);\n g.crypto.getRandomValues(buf);\n randHigh = buf[0]!;\n randLow = buf[1]!;\n } else {\n // Node fallback — require lazily so browser bundlers can tree-shake.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const nodeCrypto = require(\"node:crypto\") as {\n randomBytes: (n: number) => Buffer;\n };\n const b = nodeCrypto.randomBytes(8);\n randHigh = b.readUInt32BE(0);\n randLow = b.readUInt32BE(4);\n }\n\n const rand =\n (BigInt(randHigh) << 32n) | BigInt(randLow);\n return (tsMs << 64n) | rand;\n}\n\n/**\n * Validate hex-encoded signature shape: starts with `0x`, even length\n * AFTER the prefix, and 65 bytes (`0x` + 130 hex) minimum. Allows\n * larger sizes for EIP-1271 contract signatures.\n */\nfunction isValidHexSignature(sig: string): boolean {\n if (!sig.startsWith(\"0x\")) return false;\n const hex = sig.slice(2);\n // Must be even length (each byte = 2 hex chars).\n if (hex.length % 2 !== 0) return false;\n // ECDSA = 65 bytes = 130 hex chars. EIP-1271 may be longer; reject\n // shorter lengths.\n if (hex.length < 130) return false;\n // Hex regex (0–9, a–f, A–F).\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\nexport const SPONSOR_AUTH_DOMAIN_NAME = \"PafiSponsorAuth\";\n\nexport const SPONSOR_AUTH_TYPES = {\n SponsorAuth: [\n { name: \"chainId\", type: \"uint256\" },\n { name: \"sender\", type: \"address\" },\n { name: \"callDataHash\", type: \"bytes32\" },\n { name: \"nonce\", type: \"uint256\" },\n { name: \"expiresAt\", type: \"uint256\" },\n { name: \"scenario\", type: \"string\" },\n { name: \"issuerId\", type: \"string\" },\n ],\n} as const;\n\nexport interface SponsorAuthPayload {\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n nonce: bigint;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface SponsorAuthVerifyResult {\n ok: boolean;\n recoveredAddress?: Address;\n reason?: \"EXPIRED\" | \"INVALID_SIGNER\" | \"INVALID_SIGNATURE_FORMAT\";\n}\n\n/**\n * Domain anchor for SponsorAuth EIP-712 signatures. The relayer service\n * is off-chain (no on-chain SponsorRelayer contract), so we bind to a\n * stable PAFI-controlled marker registered as `SPONSOR_AUTH_DOMAIN_ANCHOR`\n * in the relayer config. This rules out cross-system replay (a sibling\n * EIP-712 payload with the same field names cannot validate against\n * this domain) and gives wallets a meaningful \"verifying contract\"\n * preview instead of `0x0`.\n *\n * If you ever deploy an on-chain `SponsorRelayer` contract, switch this\n * to its address.\n */\n// Bytes 1-4 = \"PAFI\" (0x50414649); bytes 5-12 = \"SPONSOR_\" (0x53504F4E534F525F);\n// bytes 13-19 = padding; byte 20 = version tag (0x01). Total = 20 bytes.\n// Stable, system-specific marker. To migrate to a real EOA / on-chain\n// SponsorRelayer contract, replace this constant and bump `version` to \"2\".\nexport const SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET: Address =\n \"0x5041464953504F4E534F525F0000000000000001\" as Address;\n\nconst SPONSOR_AUTH_DOMAIN_ANCHORS: Record<number, Address> = {\n // Base mainnet — uses the literal-tag anchor above. To migrate to a\n // real EOA / contract, replace this entry and bump `version` to \"2\"\n // so existing sigs become invalid.\n 8453: SPONSOR_AUTH_DOMAIN_ANCHOR_BASE_MAINNET,\n};\n\nexport function getSponsorAuthDomainAnchor(chainId: number): Address {\n const anchor = SPONSOR_AUTH_DOMAIN_ANCHORS[chainId];\n if (!anchor) {\n throw new Error(\n `buildSponsorAuthDomain: no SponsorAuth domain anchor configured for chainId ${chainId}. ` +\n `Add an entry to SPONSOR_AUTH_DOMAIN_ANCHORS in @pafi-dev/core/auth/sponsorAuth.ts.`,\n );\n }\n return anchor;\n}\n\nexport function buildSponsorAuthDomain(chainId: number) {\n return {\n name: SPONSOR_AUTH_DOMAIN_NAME,\n version: \"1\",\n chainId,\n verifyingContract: getSponsorAuthDomainAnchor(chainId),\n };\n}\n\nfunction buildMessage(payload: SponsorAuthPayload) {\n return {\n chainId: BigInt(payload.chainId),\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce,\n expiresAt: BigInt(payload.expiresAt),\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport function buildSponsorAuthTypedData(payload: SponsorAuthPayload) {\n return {\n domain: buildSponsorAuthDomain(payload.chainId),\n types: SPONSOR_AUTH_TYPES,\n primaryType: \"SponsorAuth\" as const,\n message: buildMessage(payload),\n };\n}\n\nexport function computeCallDataHash(callData: Hex): Hex {\n return keccak256(callData);\n}\n\nexport async function signSponsorAuth(\n wallet: WalletClient,\n payload: SponsorAuthPayload,\n): Promise<Hex> {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n return wallet.signTypedData({\n account: wallet.account!,\n domain,\n types,\n primaryType,\n message,\n });\n}\n\nexport interface BuiltSponsorAuth {\n sig: Hex;\n chainId: number;\n sender: Address;\n callDataHash: Hex;\n /** Decimal-string for JSON transport (bigint not safely serializable). */\n nonce: string;\n expiresAt: number;\n scenario: string;\n issuerId: string;\n}\n\nexport interface BuildSponsorAuthParams {\n /** User EOA the sponsorship is for. */\n userAddress: Address;\n /** UserOp `callData` to bind the auth to (hashed via keccak256). */\n callData: Hex;\n /** Chain id the UserOp will execute on. */\n chainId: number;\n /** Scenario tag for logs / rate-limiter (`mint` / `burn` / `swap` / etc.). */\n scenario: string;\n /** Issuer id (matches `pafi_issuers` row in PAFI's sponsor-relayer). */\n issuerId: string;\n /** Issuer signer wallet (HSM/KMS-backed in production). */\n issuerSignerWallet: WalletClient;\n /** Validity window in seconds. Default 600 (10 min). */\n expiresInSeconds?: number;\n /**\n * Optional explicit nonce. When omitted, defaults to a unique\n * `Date.now() * 1e6 + random` — matches the gg56 reference impl.\n * Override for test fixtures or when chaining replay-protection.\n */\n nonce?: bigint;\n}\n\n/**\n * Build, sign, and serialize a `SponsorAuth` payload in one call.\n * Replaces the ~30 LoC private `buildSponsorAuth` helper that every\n * issuer would otherwise reimplement on each sponsored endpoint.\n *\n * Output is JSON-safe (`nonce` as decimal string) and matches the\n * shape sponsor-relayer's `/paymaster/sponsor` accepts as the\n * `sponsorAuth` field.\n *\n * Issuer typically calls this from each sponsored controller route:\n *\n * ```ts\n * const sponsorAuth = await buildAndSignSponsorAuth({\n * userAddress: user.userAddress,\n * callData: userOp.callData,\n * chainId,\n * scenario: 'mint',\n * issuerId: this.config.get('PAFI_ISSUER_ID'),\n * issuerSignerWallet: this.issuerSignerWallet,\n * });\n * return { ...response, sponsorAuth };\n * ```\n */\nexport async function buildAndSignSponsorAuth(\n params: BuildSponsorAuthParams,\n): Promise<BuiltSponsorAuth> {\n const expiresAt =\n Math.floor(Date.now() / 1000) + (params.expiresInSeconds ?? 600);\n\n const nonce = params.nonce ?? generateSponsorAuthNonce();\n\n const payload: SponsorAuthPayload = {\n chainId: params.chainId,\n sender: params.userAddress,\n callDataHash: computeCallDataHash(params.callData),\n nonce,\n expiresAt,\n scenario: params.scenario,\n issuerId: params.issuerId,\n };\n\n const sig = await signSponsorAuth(params.issuerSignerWallet, payload);\n\n return {\n sig,\n chainId: payload.chainId,\n sender: payload.sender,\n callDataHash: payload.callDataHash,\n nonce: payload.nonce.toString(),\n expiresAt: payload.expiresAt,\n scenario: payload.scenario,\n issuerId: payload.issuerId,\n };\n}\n\nexport async function verifySponsorAuth(\n payload: SponsorAuthPayload,\n signature: Hex,\n expectedSigner: Address,\n): Promise<SponsorAuthVerifyResult> {\n const nowSec = Math.floor(Date.now() / 1000);\n if (payload.expiresAt < nowSec) {\n return { ok: false, reason: \"EXPIRED\" };\n }\n\n // v0.7.1 — strict hex format check before deferring to viem. Previous\n // length-only `< 132` accepted arbitrary 132+ char strings even when\n // not valid hex; recoverTypedDataAddress would throw and the catch\n // collapsed both cases to INVALID_SIGNATURE_FORMAT. Verify hex shape\n // up front + accept ECDSA (65 byte = 132 hex char) AND EIP-1271\n // (variable-length, must still be valid hex).\n if (!isValidHexSignature(signature)) {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n let recovered: Address;\n try {\n const { domain, types, primaryType, message } =\n buildSponsorAuthTypedData(payload);\n recovered = await recoverTypedDataAddress({\n domain,\n types,\n primaryType,\n message,\n signature,\n });\n } catch {\n return { ok: false, reason: \"INVALID_SIGNATURE_FORMAT\" };\n }\n\n if (recovered.toLowerCase() !== expectedSigner.toLowerCase()) {\n return { ok: false, reason: \"INVALID_SIGNER\", recoveredAddress: recovered };\n }\n\n return { ok: true, recoveredAddress: recovered };\n}\n"],"mappings":"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","names":[]}
|