{"version":3,"file":"authenticate-aYY4r3N8.mjs","names":["CHALLENGE_TTL"],"sources":["../src/tokens.ts","../src/passkey/register.ts","../src/passkey/authenticate.ts"],"sourcesContent":["/**\n * Secure token utilities\n *\n * Crypto via Oslo.js (@oslojs/crypto). Base64url via @oslojs/encoding.\n *\n * Tokens are opaque random values. We store only the SHA-256 hash in the database.\n */\n\nimport { hmac } from \"@oslojs/crypto/hmac\";\nimport { sha256, SHA256 } from \"@oslojs/crypto/sha2\";\nimport { constantTimeEqual } from \"@oslojs/crypto/subtle\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\n\nconst TOKEN_BYTES = 32; // 256 bits of entropy\n\n// ---------------------------------------------------------------------------\n// API Token Prefixes\n// ---------------------------------------------------------------------------\n\n/** Valid API token prefixes */\nexport const TOKEN_PREFIXES = {\n\tPAT: \"ec_pat_\",\n\tOAUTH_ACCESS: \"ec_oat_\",\n\tOAUTH_REFRESH: \"ec_ort_\",\n} as const;\n\n// ---------------------------------------------------------------------------\n// Scopes\n// ---------------------------------------------------------------------------\n\n/** All valid API token scopes */\nexport const VALID_SCOPES = [\n\t\"content:read\",\n\t\"content:write\",\n\t\"media:read\",\n\t\"media:write\",\n\t\"schema:read\",\n\t\"schema:write\",\n\t\"taxonomies:manage\",\n\t\"menus:manage\",\n\t\"settings:read\",\n\t\"settings:manage\",\n\t\"mcp:tools\",\n\t\"admin\",\n] as const;\n\nexport type ApiTokenScope = (typeof VALID_SCOPES)[number] | `mcp:tools:${string}`;\n\nconst PLUGIN_MCP_SCOPE_PATTERN = /^mcp:tools:[a-z][a-z0-9_-]*$/;\n\nexport function isValidScope(scope: string): scope is ApiTokenScope {\n\treturn (\n\t\t(VALID_SCOPES as readonly string[]).includes(scope) || PLUGIN_MCP_SCOPE_PATTERN.test(scope)\n\t);\n}\n\n/**\n * Validate that scopes are all valid.\n * Returns the invalid scopes, or empty array if all valid.\n */\nexport function validateScopes(scopes: string[]): string[] {\n\treturn scopes.filter((scope) => !isValidScope(scope));\n}\n\n/**\n * Scope grants — when a token holds the key scope, it implicitly grants\n * the listed scopes too. This keeps existing tokens working when more\n * granular scopes are introduced.\n *\n * Specifically, `content:write` was historically the only scope we checked\n * for menu and taxonomy mutations. After splitting those out into\n * `menus:manage` and `taxonomies:manage`, existing PATs with `content:write`\n * continue to work via this grant table.\n *\n * Lookup is one-hop — chaining (`A → B → C`) is NOT supported. If a chain\n * is needed, expand the values explicitly. Backed by a `Map` rather than a\n * plain object so prototype-chain keys (`__proto__`, `constructor`, etc.)\n * can't smuggle non-array values through bracket access.\n */\nconst IMPLICIT_SCOPE_GRANTS = new Map<string, readonly string[]>([\n\t[\"content:write\", [\"menus:manage\", \"taxonomies:manage\"]],\n]);\n\n/**\n * Check if a set of scopes includes a required scope.\n *\n * The `admin` scope grants access to everything. `content:write` implicitly\n * grants `menus:manage` and `taxonomies:manage` to preserve backwards\n * compatibility with PATs issued before those scopes were split out.\n */\nexport function hasScope(scopes: string[], required: string): boolean {\n\tif (required === \"mcp:tools\" || required.startsWith(\"mcp:tools:\")) {\n\t\treturn scopes.includes(\"mcp:tools\") || scopes.includes(required);\n\t}\n\tif (scopes.includes(\"admin\")) return true;\n\tif (scopes.includes(required)) return true;\n\tfor (const held of scopes) {\n\t\tconst granted = IMPLICIT_SCOPE_GRANTS.get(held);\n\t\tif (granted?.includes(required)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Generate a cryptographically secure random token\n * Returns base64url-encoded string (URL-safe)\n */\nexport function generateToken(): string {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Hash a token for storage\n * We never store raw tokens - only their SHA-256 hash\n */\nexport function hashToken(token: string): string {\n\tconst bytes = decodeBase64urlIgnorePadding(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Generate a token and its hash together\n */\nexport function generateTokenWithHash(): { token: string; hash: string } {\n\tconst token = generateToken();\n\tconst hash = hashToken(token);\n\treturn { token, hash };\n}\n\n/**\n * Generate a session ID (shorter, for cookie storage)\n */\nexport function generateSessionId(): string {\n\tconst bytes = new Uint8Array(20); // 160 bits\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Generate an auth secret for configuration\n */\nexport function generateAuthSecret(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n// ---------------------------------------------------------------------------\n// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a prefixed API token and its hash.\n * Returns the raw token (shown once to the user), the hash (stored server-side),\n * and a display prefix (for identification in UIs/logs).\n *\n * Uses oslo/crypto for SHA-256 hashing.\n */\nexport function generatePrefixedToken(prefix: string): {\n\traw: string;\n\thash: string;\n\tprefix: string;\n} {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\n\tconst encoded = encodeBase64urlNoPadding(bytes);\n\tconst raw = `${prefix}${encoded}`;\n\tconst hash = hashPrefixedToken(raw);\n\n\t// First few chars for identification in UIs\n\tconst displayPrefix = raw.slice(0, prefix.length + 4);\n\n\treturn { raw, hash, prefix: displayPrefix };\n}\n\n/**\n * Hash a prefixed API token for storage/lookup.\n * Hashes the full prefixed token string via SHA-256, returns base64url (no padding).\n */\nexport function hashPrefixedToken(token: string): string {\n\tconst bytes = new TextEncoder().encode(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n// ---------------------------------------------------------------------------\n// PKCE (RFC 7636) — server-side verification\n// ---------------------------------------------------------------------------\n\n/**\n * Compute an S256 PKCE code challenge from a code verifier.\n * Used server-side to verify that code_verifier matches the stored code_challenge.\n *\n * Equivalent to: BASE64URL(SHA256(ASCII(code_verifier)))\n */\nexport function computeS256Challenge(codeVerifier: string): string {\n\tconst hash = sha256(new TextEncoder().encode(codeVerifier));\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Constant-time comparison to prevent timing attacks\n */\nexport function secureCompare(a: string, b: string): boolean {\n\tconst text = new TextEncoder();\n\tconst salt = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES));\n\tconst hash = (str: string) => hmac(SHA256, salt, text.encode(str));\n\n\treturn constantTimeEqual(hash(a), hash(b));\n}\n\n// ============================================================================\n// Encryption utilities (for storing OAuth secrets)\n// ============================================================================\n\nconst ALGORITHM = \"AES-GCM\";\nconst IV_BYTES = 12;\n\n/**\n * Derive an encryption key from the auth secret\n */\nasync function deriveKey(secret: string): Promise<CryptoKey> {\n\tconst decoded = decodeBase64urlIgnorePadding(secret);\n\t// Create a new ArrayBuffer to ensure compatibility with crypto.subtle\n\tconst buffer = new Uint8Array(decoded).buffer;\n\tconst keyMaterial = await crypto.subtle.importKey(\"raw\", buffer, \"PBKDF2\", false, [\"deriveKey\"]);\n\n\treturn crypto.subtle.deriveKey(\n\t\t{\n\t\t\tname: \"PBKDF2\",\n\t\t\tsalt: new TextEncoder().encode(\"emdash-auth-v1\"),\n\t\t\titerations: 100000,\n\t\t\thash: \"SHA-256\",\n\t\t},\n\t\tkeyMaterial,\n\t\t{ name: ALGORITHM, length: 256 },\n\t\tfalse,\n\t\t[\"encrypt\", \"decrypt\"],\n\t);\n}\n\n/**\n * Encrypt a value using AES-GCM\n */\nexport async function encrypt(plaintext: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n\tconst encoded = new TextEncoder().encode(plaintext);\n\n\tconst ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);\n\n\t// Prepend IV to ciphertext\n\tconst combined = new Uint8Array(iv.length + ciphertext.byteLength);\n\tcombined.set(iv);\n\tcombined.set(new Uint8Array(ciphertext), iv.length);\n\n\treturn encodeBase64urlNoPadding(combined);\n}\n\n/**\n * Decrypt a value encrypted with encrypt()\n */\nexport async function decrypt(encrypted: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst combined = decodeBase64urlIgnorePadding(encrypted);\n\n\tconst iv = combined.slice(0, IV_BYTES);\n\tconst ciphertext = combined.slice(IV_BYTES);\n\n\tconst decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);\n\n\treturn new TextDecoder().decode(decrypted);\n}\n","/**\n * Passkey registration (credential creation)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/registration\n */\n\nimport { ECDSAPublicKey, p256 } from \"@oslojs/crypto/ecdsa\";\nimport { RSAPublicKey } from \"@oslojs/crypto/rsa\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAttestationObject,\n\tparseClientDataJSON,\n\tcoseAlgorithmES256,\n\tcoseAlgorithmRS256,\n\tcoseEllipticCurveP256,\n\tClientDataType,\n\tAttestationStatementFormat,\n\tCOSEKeyType,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, NewCredential, AuthAdapter, User, DeviceType } from \"../types.js\";\nimport type {\n\tRegistrationOptions,\n\tRegistrationResponse,\n\tVerifiedRegistration,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\nexport type { PasskeyConfig };\n\n/**\n * Generate registration options for creating a new passkey\n */\nexport async function generateRegistrationOptions(\n\tconfig: PasskeyConfig,\n\tuser: Pick<User, \"id\" | \"email\" | \"name\">,\n\texistingCredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<RegistrationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"registration\",\n\t\tuserId: user.id,\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\t// Encode user ID as base64url\n\tconst userIdBytes = new TextEncoder().encode(user.id);\n\tconst userIdEncoded = encodeBase64urlNoPadding(userIdBytes);\n\n\treturn {\n\t\tchallenge,\n\t\trp: {\n\t\t\tname: config.rpName,\n\t\t\tid: config.rpId,\n\t\t},\n\t\tuser: {\n\t\t\tid: userIdEncoded,\n\t\t\tname: user.email,\n\t\t\tdisplayName: user.name || user.email,\n\t\t},\n\t\tpubKeyCredParams: [\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmES256 }, // ES256 (-7)\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmRS256 }, // RS256 (-257)\n\t\t],\n\t\ttimeout: 60000,\n\t\tattestation: \"none\", // We don't need attestation for our use case\n\t\tauthenticatorSelection: {\n\t\t\tresidentKey: \"preferred\", // Allow discoverable credentials\n\t\t\tuserVerification: \"preferred\",\n\t\t},\n\t\texcludeCredentials: existingCredentials.map((cred) => ({\n\t\t\ttype: \"public-key\" as const,\n\t\t\tid: cred.id,\n\t\t\ttransports: cred.transports,\n\t\t})),\n\t};\n}\n\n/**\n * Verify a registration response and extract credential data\n */\nexport async function verifyRegistrationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: RegistrationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedRegistration> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst attestationObject = decodeBase64urlIgnorePadding(response.response.attestationObject);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data\n\tif (clientData.type !== ClientDataType.Create) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"registration\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin against the accepted list\n\tif (!config.origins.includes(clientData.origin)) {\n\t\tthrow new Error(`Invalid origin: ${clientData.origin} not in [${config.origins.join(\", \")}]`);\n\t}\n\n\t// Parse attestation object\n\tconst attestation = parseAttestationObject(attestationObject);\n\n\t// We only support 'none' attestation for simplicity\n\tif (attestation.attestationStatement.format !== AttestationStatementFormat.None) {\n\t\t// For other formats, we'd need to verify the attestation statement\n\t\t// For now, we just ignore it and trust the credential\n\t}\n\n\tconst { authenticatorData } = attestation;\n\n\t// Verify RP ID hash\n\tif (!authenticatorData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authenticatorData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Extract credential data\n\tif (!authenticatorData.credential) {\n\t\tthrow new Error(\"No credential data in attestation\");\n\t}\n\n\tconst { credential } = authenticatorData;\n\n\t// Verify algorithm is supported and encode public key\n\t// Supports ES256 (ECDSA P-256, stored as SEC1) and RS256 (RSA, stored as PKIX)\n\tconst algorithm = credential.publicKey.algorithm();\n\tlet encodedPublicKey: Uint8Array;\n\n\tif (algorithm === coseAlgorithmES256) {\n\t\t// Verify EC2 key type for ES256\n\t\tif (credential.publicKey.type() !== COSEKeyType.EC2) {\n\t\t\tthrow new Error(\"Expected EC2 key type for ES256\");\n\t\t}\n\t\tconst cosePublicKey = credential.publicKey.ec2();\n\t\tif (cosePublicKey.curve !== coseEllipticCurveP256) {\n\t\t\tthrow new Error(\"Expected P-256 curve for ES256\");\n\t\t}\n\t\t// Encode as SEC1 uncompressed format for storage\n\t\tencodedPublicKey = new ECDSAPublicKey(\n\t\t\tp256,\n\t\t\tcosePublicKey.x,\n\t\t\tcosePublicKey.y,\n\t\t).encodeSEC1Uncompressed();\n\t} else if (algorithm === coseAlgorithmRS256) {\n\t\t// Verify RSA key type for RS256\n\t\tif (credential.publicKey.type() !== COSEKeyType.RSA) {\n\t\t\tthrow new Error(\"Expected RSA key type for RS256\");\n\t\t}\n\t\tconst cosePublicKey = credential.publicKey.rsa();\n\t\t// Encode as PKIX format for storage\n\t\tencodedPublicKey = new RSAPublicKey(cosePublicKey.n, cosePublicKey.e).encodePKIX();\n\t} else {\n\t\tthrow new Error(`Unsupported credential algorithm: ${algorithm}`);\n\t}\n\n\t// Determine device type and backup status\n\t// Note: oslo webauthn doesn't expose backup flags, so we default to singleDevice\n\t// In practice, most modern passkeys are multi-device (e.g., iCloud Keychain, Google Password Manager)\n\tconst deviceType: DeviceType = \"singleDevice\";\n\tconst backedUp = false;\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tpublicKey: encodedPublicKey,\n\t\talgorithm,\n\t\tcounter: authenticatorData.signatureCounter,\n\t\tdeviceType,\n\t\tbackedUp,\n\t\ttransports: response.response.transports ?? [],\n\t};\n}\n\n/**\n * Register a new passkey for a user\n */\nexport async function registerPasskey(\n\tadapter: AuthAdapter,\n\tuserId: string,\n\tverified: VerifiedRegistration,\n\tname?: string,\n): Promise<Credential> {\n\t// Check credential limit\n\tconst count = await adapter.countCredentialsByUserId(userId);\n\tif (count >= 10) {\n\t\tthrow new Error(\"Maximum number of passkeys reached (10)\");\n\t}\n\n\t// Check if credential already exists\n\tconst existing = await adapter.getCredentialById(verified.credentialId);\n\tif (existing) {\n\t\tthrow new Error(\"Credential already registered\");\n\t}\n\n\tconst newCredential: NewCredential = {\n\t\tid: verified.credentialId,\n\t\tuserId,\n\t\tpublicKey: verified.publicKey,\n\t\talgorithm: verified.algorithm,\n\t\tcounter: verified.counter,\n\t\tdeviceType: verified.deviceType,\n\t\tbackedUp: verified.backedUp,\n\t\ttransports: verified.transports,\n\t\tname,\n\t};\n\n\treturn adapter.createCredential(newCredential);\n}\n","/**\n * Passkey authentication (credential assertion)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/authentication\n */\n\nimport {\n\tverifyECDSASignature,\n\tp256,\n\tdecodeSEC1PublicKey,\n\tdecodePKIXECDSASignature,\n} from \"@oslojs/crypto/ecdsa\";\nimport {\n\tdecodePKIXRSAPublicKey,\n\tverifyRSASSAPKCS1v15Signature,\n\tsha256ObjectIdentifier,\n} from \"@oslojs/crypto/rsa\";\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAuthenticatorData,\n\tparseClientDataJSON,\n\tClientDataType,\n\tcreateAssertionSignatureMessage,\n\tcoseAlgorithmES256,\n\tcoseAlgorithmRS256,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, AuthAdapter, User } from \"../types.js\";\nimport type {\n\tAuthenticationOptions,\n\tAuthenticationResponse,\n\tVerifiedAuthentication,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\nexport type PasskeyAuthenticationErrorCode =\n\t| \"credential_not_found\"\n\t| \"invalid_response\"\n\t| \"challenge_not_found\"\n\t| \"invalid_challenge_type\"\n\t| \"challenge_expired\"\n\t| \"invalid_client_data_type\"\n\t| \"invalid_origin\"\n\t| \"invalid_rp_id_hash\"\n\t| \"user_presence_not_verified\"\n\t| \"invalid_signature_counter\"\n\t| \"invalid_signature\"\n\t| \"unsupported_algorithm\"\n\t| \"user_not_found\";\n\nexport class PasskeyAuthenticationError extends Error {\n\tconstructor(\n\t\tpublic code: PasskeyAuthenticationErrorCode,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"PasskeyAuthenticationError\";\n\t}\n}\n\nfunction invalidPasskeyResponseError(): PasskeyAuthenticationError {\n\treturn new PasskeyAuthenticationError(\"invalid_response\", \"Invalid passkey response\");\n}\n\nfunction decodeAuthenticationResponse(response: AuthenticationResponse) {\n\ttry {\n\t\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\t\tconst authenticatorData = decodeBase64urlIgnorePadding(response.response.authenticatorData);\n\t\tconst signature = decodeBase64urlIgnorePadding(response.response.signature);\n\t\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t\treturn { clientDataJSON, authenticatorData, signature, clientData };\n\t} catch {\n\t\tthrow invalidPasskeyResponseError();\n\t}\n}\n\nfunction parseAuthenticationData(authenticatorData: Uint8Array) {\n\ttry {\n\t\treturn parseAuthenticatorData(authenticatorData);\n\t} catch {\n\t\tthrow invalidPasskeyResponseError();\n\t}\n}\n\nfunction decodeAssertionSignature(signature: Uint8Array) {\n\ttry {\n\t\treturn decodePKIXECDSASignature(signature);\n\t} catch {\n\t\tthrow invalidPasskeyResponseError();\n\t}\n}\n\n/**\n * Generate authentication options for signing in with a passkey\n */\nexport async function generateAuthenticationOptions(\n\tconfig: PasskeyConfig,\n\tcredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<AuthenticationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"authentication\",\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\treturn {\n\t\tchallenge,\n\t\trpId: config.rpId,\n\t\ttimeout: 60000,\n\t\tuserVerification: \"preferred\",\n\t\tallowCredentials:\n\t\t\tcredentials.length > 0\n\t\t\t\t? credentials.map((cred) => ({\n\t\t\t\t\t\ttype: \"public-key\" as const,\n\t\t\t\t\t\tid: cred.id,\n\t\t\t\t\t\ttransports: cred.transports,\n\t\t\t\t\t}))\n\t\t\t\t: undefined, // Empty = allow any discoverable credential\n\t};\n}\n\n/**\n * Verify an authentication response\n */\nexport async function verifyAuthenticationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: AuthenticationResponse,\n\tcredential: Credential,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedAuthentication> {\n\tconst { clientDataJSON, authenticatorData, signature, clientData } =\n\t\tdecodeAuthenticationResponse(response);\n\n\t// Verify client data type\n\tif (clientData.type !== ClientDataType.Get) {\n\t\tthrow new PasskeyAuthenticationError(\"invalid_client_data_type\", \"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new PasskeyAuthenticationError(\"challenge_not_found\", \"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"authentication\") {\n\t\tthrow new PasskeyAuthenticationError(\"invalid_challenge_type\", \"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new PasskeyAuthenticationError(\"challenge_expired\", \"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin against the accepted list\n\tif (!config.origins.includes(clientData.origin)) {\n\t\tthrow new PasskeyAuthenticationError(\n\t\t\t\"invalid_origin\",\n\t\t\t`Invalid origin: ${clientData.origin} not in [${config.origins.join(\", \")}]`,\n\t\t);\n\t}\n\n\t// Parse authenticator data\n\tconst authData = parseAuthenticationData(authenticatorData);\n\n\t// Verify RP ID hash\n\tif (!authData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new PasskeyAuthenticationError(\"invalid_rp_id_hash\", \"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authData.userPresent) {\n\t\tthrow new PasskeyAuthenticationError(\n\t\t\t\"user_presence_not_verified\",\n\t\t\t\"User presence not verified\",\n\t\t);\n\t}\n\n\t// Verify counter (prevent replay attacks)\n\tif (authData.signatureCounter !== 0 && authData.signatureCounter <= credential.counter) {\n\t\tthrow new PasskeyAuthenticationError(\n\t\t\t\"invalid_signature_counter\",\n\t\t\t\"Invalid signature counter - possible cloned authenticator\",\n\t\t);\n\t}\n\n\t// Create the message that was signed\n\tconst signatureMessage = createAssertionSignatureMessage(authenticatorData, clientDataJSON);\n\n\t// Ensure public key is a Uint8Array (may come as Buffer from some DB drivers)\n\tconst publicKeyBytes =\n\t\tcredential.publicKey instanceof Uint8Array\n\t\t\t? credential.publicKey\n\t\t\t: new Uint8Array(credential.publicKey);\n\n\t// Verify signature based on the stored algorithm\n\tlet signatureValid = false;\n\tconst hash = sha256(signatureMessage);\n\n\tif (credential.algorithm === coseAlgorithmES256) {\n\t\t// Verify ECDSA signature\n\t\tconst ecdsaPublicKey = decodeSEC1PublicKey(p256, publicKeyBytes);\n\t\tconst ecdsaSignature = decodeAssertionSignature(signature);\n\t\tsignatureValid = verifyECDSASignature(ecdsaPublicKey, hash, ecdsaSignature);\n\t} else if (credential.algorithm === coseAlgorithmRS256) {\n\t\t// Verify RSA signature\n\t\tconst rsaPublicKey = decodePKIXRSAPublicKey(publicKeyBytes);\n\t\tsignatureValid = verifyRSASSAPKCS1v15Signature(\n\t\t\trsaPublicKey,\n\t\t\tsha256ObjectIdentifier,\n\t\t\thash,\n\t\t\tsignature,\n\t\t);\n\t} else {\n\t\tthrow new PasskeyAuthenticationError(\n\t\t\t\"unsupported_algorithm\",\n\t\t\t`Unsupported credential algorithm: ${credential.algorithm}`,\n\t\t);\n\t}\n\n\tif (!signatureValid) {\n\t\tthrow new PasskeyAuthenticationError(\"invalid_signature\", \"Invalid signature\");\n\t}\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tnewCounter: authData.signatureCounter,\n\t};\n}\n\n/**\n * Authenticate a user with a passkey\n */\nexport async function authenticateWithPasskey(\n\tconfig: PasskeyConfig,\n\tadapter: AuthAdapter,\n\tresponse: AuthenticationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<User> {\n\t// Find the credential\n\tconst credential = await adapter.getCredentialById(response.id);\n\tif (!credential) {\n\t\tthrow new PasskeyAuthenticationError(\"credential_not_found\", \"Credential not found\");\n\t}\n\n\t// Verify the response\n\tconst verified = await verifyAuthenticationResponse(config, response, credential, challengeStore);\n\n\t// Update counter\n\tawait adapter.updateCredentialCounter(verified.credentialId, verified.newCounter);\n\n\t// Get the user\n\tconst user = await adapter.getUserById(credential.userId);\n\tif (!user) {\n\t\tthrow new PasskeyAuthenticationError(\"user_not_found\", \"User not found\");\n\t}\n\n\treturn user;\n}\n"],"mappings":"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"}