UNPKG

@opendatalabs/vana-sdk

Version:

A TypeScript library for interacting with Vana Network smart contracts.

1 lines 6.25 kB
{"version":3,"sources":["../../src/auth/pkce.ts"],"sourcesContent":["/**\n * PKCE (Proof Key for Code Exchange) primitives per RFC 7636.\n *\n * @remarks\n * Implements the S256 challenge method only. All functions are pure and\n * isomorphic — they rely on the `crypto.getRandomValues` and\n * `crypto.subtle` Web Crypto APIs available as globals in browsers and\n * Node.js (>= 20).\n *\n * @category Auth\n * @module auth/pkce\n */\n\n// RFC 7636 §4.1 unreserved characters: ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\nconst PKCE_VERIFIER_ALPHABET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\n\nconst PKCE_VERIFIER_MIN_LENGTH = 43;\nconst PKCE_VERIFIER_MAX_LENGTH = 128;\nconst PKCE_VERIFIER_DEFAULT_LENGTH = 64;\n\n/**\n * RFC 7636 §4.1 verifier shape: 43-128 unreserved chars.\n *\n * @category Auth\n */\nexport const PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;\n\n/**\n * S256 challenge shape: 43-char base64url (SHA-256 = 32 bytes → 43 chars\n * after base64url-no-padding encoding).\n *\n * @category Auth\n */\nexport const PKCE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;\n\n/**\n * Throws {@link RangeError} unless `verifier` matches RFC 7636 §4.1.\n *\n * @category Auth\n */\nexport function assertValidPkceVerifier(verifier: string): void {\n if (!PKCE_VERIFIER_PATTERN.test(verifier)) {\n throw new RangeError(\n `PKCE verifier must match RFC 7636 §4.1: ${PKCE_VERIFIER_MIN_LENGTH}-${PKCE_VERIFIER_MAX_LENGTH} unreserved chars [A-Za-z0-9._~-]`,\n );\n }\n}\n\n/**\n * Generates a cryptographically random PKCE code verifier.\n *\n * @param length - Verifier length in characters. Must be between 43 and 128\n * (RFC 7636 §4.1). Defaults to 64.\n * @returns A string of the requested length using only the RFC-allowed\n * unreserved alphabet.\n */\nexport function generatePkceVerifier(\n length: number = PKCE_VERIFIER_DEFAULT_LENGTH,\n): string {\n if (\n !Number.isInteger(length) ||\n length < PKCE_VERIFIER_MIN_LENGTH ||\n length > PKCE_VERIFIER_MAX_LENGTH\n ) {\n throw new RangeError(\n `PKCE verifier length must be an integer between ${PKCE_VERIFIER_MIN_LENGTH} and ${PKCE_VERIFIER_MAX_LENGTH}`,\n );\n }\n\n const alphabetLen = PKCE_VERIFIER_ALPHABET.length;\n // Reject-sample to avoid modulo bias from a non-power-of-two alphabet (66).\n const acceptCutoff = Math.floor(256 / alphabetLen) * alphabetLen;\n\n const out = new Array<string>(length);\n let filled = 0;\n const buffer = new Uint8Array(length * 2);\n\n while (filled < length) {\n crypto.getRandomValues(buffer);\n for (let i = 0; i < buffer.length && filled < length; i++) {\n const byte = buffer[i] as number;\n if (byte < acceptCutoff) {\n out[filled++] = PKCE_VERIFIER_ALPHABET[byte % alphabetLen] as string;\n }\n }\n }\n\n return out.join(\"\");\n}\n\n/**\n * Computes the S256 PKCE code challenge for a verifier.\n *\n * @param verifier - The PKCE code verifier.\n * @returns The base64url-encoded SHA-256 hash of the verifier (no padding).\n */\nexport async function computePkceChallenge(verifier: string): Promise<string> {\n assertValidPkceVerifier(verifier);\n const bytes = new TextEncoder().encode(verifier);\n const digest = await crypto.subtle.digest(\"SHA-256\", bytes);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\n/**\n * Verifies that a verifier hashes to the given S256 challenge.\n *\n * @param verifier - The PKCE code verifier presented by the client.\n * @param challenge - The previously stored S256 challenge.\n * @returns `true` when the verifier matches; `false` otherwise.\n */\nexport async function verifyPkceChallenge(\n verifier: string,\n challenge: string,\n): Promise<boolean> {\n if (!PKCE_VERIFIER_PATTERN.test(verifier)) return false;\n if (!PKCE_CHALLENGE_PATTERN.test(challenge)) return false;\n const computed = await computePkceChallenge(verifier);\n return constantTimeEqualString(computed, challenge);\n}\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number);\n }\n // btoa is available in Node 16+ and all browsers.\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction constantTimeEqualString(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return result === 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,MAAM,yBACJ;AAEF,MAAM,2BAA2B;AACjC,MAAM,2BAA2B;AACjC,MAAM,+BAA+B;AAO9B,MAAM,wBAAwB;AAQ9B,MAAM,yBAAyB;AAO/B,SAAS,wBAAwB,UAAwB;AAC9D,MAAI,CAAC,sBAAsB,KAAK,QAAQ,GAAG;AACzC,UAAM,IAAI;AAAA,MACR,8CAA2C,wBAAwB,IAAI,wBAAwB;AAAA,IACjG;AAAA,EACF;AACF;AAUO,SAAS,qBACd,SAAiB,8BACT;AACR,MACE,CAAC,OAAO,UAAU,MAAM,KACxB,SAAS,4BACT,SAAS,0BACT;AACA,UAAM,IAAI;AAAA,MACR,mDAAmD,wBAAwB,QAAQ,wBAAwB;AAAA,IAC7G;AAAA,EACF;AAEA,QAAM,cAAc,uBAAuB;AAE3C,QAAM,eAAe,KAAK,MAAM,MAAM,WAAW,IAAI;AAErD,QAAM,MAAM,IAAI,MAAc,MAAM;AACpC,MAAI,SAAS;AACb,QAAM,SAAS,IAAI,WAAW,SAAS,CAAC;AAExC,SAAO,SAAS,QAAQ;AACtB,WAAO,gBAAgB,MAAM;AAC7B,aAAS,IAAI,GAAG,IAAI,OAAO,UAAU,SAAS,QAAQ,KAAK;AACzD,YAAM,OAAO,OAAO,CAAC;AACrB,UAAI,OAAO,cAAc;AACvB,YAAI,QAAQ,IAAI,uBAAuB,OAAO,WAAW;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAEA,SAAO,IAAI,KAAK,EAAE;AACpB;AAQA,eAAsB,qBAAqB,UAAmC;AAC5E,0BAAwB,QAAQ;AAChC,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,QAAQ;AAC/C,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK;AAC1D,SAAO,gBAAgB,IAAI,WAAW,MAAM,CAAC;AAC/C;AASA,eAAsB,oBACpB,UACA,WACkB;AAClB,MAAI,CAAC,sBAAsB,KAAK,QAAQ,EAAG,QAAO;AAClD,MAAI,CAAC,uBAAuB,KAAK,SAAS,EAAG,QAAO;AACpD,QAAM,WAAW,MAAM,qBAAqB,QAAQ;AACpD,SAAO,wBAAwB,UAAU,SAAS;AACpD;AAEA,SAAS,gBAAgB,OAA2B;AAClD,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAW;AAAA,EAClD;AAEA,SAAO,KAAK,MAAM,EACf,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAEA,SAAS,wBAAwB,GAAW,GAAoB;AAC9D,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,cAAU,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;AAAA,EAC5C;AACA,SAAO,WAAW;AACpB;","names":[]}