// WebAuthn helpers. Challenges are ALWAYS generated by the backend; the browser only // converts the base64url fields and runs navigator.credentials. Credential responses / // authenticator data are never logged. export function b64urlToBuf(s: string): ArrayBuffer { const pad = '='.repeat((4 - (s.length % 4)) % 4) const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/') const bin = atob(b64) const bytes = new Uint8Array(bin.length) for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) return bytes.buffer } export function bufToB64url(buf: ArrayBuffer): string { const bytes = new Uint8Array(buf) let s = '' for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]) return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } export function isWebAuthnSupported(): boolean { return typeof window !== 'undefined' && !!window.PublicKeyCredential } export async function platformAuthenticatorAvailable(): Promise { try { return ( isWebAuthnSupported() && (await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) ) } catch { return false } } // Backend register-options -> browser create() input. // eslint-disable-next-line @typescript-eslint/no-explicit-any export function toCreationOptions(opts: any): CredentialCreationOptions { const publicKey = { ...opts } publicKey.challenge = b64urlToBuf(opts.challenge) publicKey.user = { ...opts.user, id: b64urlToBuf(opts.user.id) } if (Array.isArray(opts.excludeCredentials)) { publicKey.excludeCredentials = opts.excludeCredentials.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any (c: any) => ({ ...c, id: b64urlToBuf(c.id) }), ) } return { publicKey } } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function toRequestOptions(opts: any): CredentialRequestOptions { const publicKey = { ...opts } publicKey.challenge = b64urlToBuf(opts.challenge) if (Array.isArray(opts.allowCredentials)) { publicKey.allowCredentials = opts.allowCredentials.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any (c: any) => ({ ...c, id: b64urlToBuf(c.id) }), ) } return { publicKey } } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function serializeRegistration(cred: PublicKeyCredential): any { const r = cred.response as AuthenticatorAttestationResponse return { id: cred.id, rawId: bufToB64url(cred.rawId), type: cred.type, response: { attestationObject: bufToB64url(r.attestationObject), clientDataJSON: bufToB64url(r.clientDataJSON), }, clientExtensionResults: cred.getClientExtensionResults?.() ?? {}, } } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function serializeAssertion(cred: PublicKeyCredential): any { const r = cred.response as AuthenticatorAssertionResponse return { id: cred.id, rawId: bufToB64url(cred.rawId), type: cred.type, response: { authenticatorData: bufToB64url(r.authenticatorData), clientDataJSON: bufToB64url(r.clientDataJSON), signature: bufToB64url(r.signature), userHandle: r.userHandle ? bufToB64url(r.userHandle) : null, }, } } export class PasskeyCancelled extends Error {} export class PasskeyUnsupported extends Error {}