import type { PasskeyAuthenticationOptions, PasskeyProvider, PasskeyRegistrationOptions, } from "../types.ts"; export type { PasskeyProvider } from "../types.ts"; export type PasskeyChallengeKind = "registration" | "authentication"; export interface PasskeyChallengeRecord { challenge: string; kind: PasskeyChallengeKind; userId?: string; rpId: string; origin: string; expiresAt: number; } /** * Durable challenge storage contract. Production deployments with more than one * process should provide a shared implementation (for example Redis or SQL). */ export interface PasskeyChallengeStore { set(key: string, value: PasskeyChallengeRecord): Promise; consume(key: string): Promise; } export class MemoryPasskeyChallengeStore implements PasskeyChallengeStore { private readonly values = new Map(); private readonly now: () => number; constructor(now: () => number = () => Date.now()) { this.now = now; } private pruneExpired(): void { const timestamp = this.now(); for (const [key, value] of this.values) { if (value.expiresAt <= timestamp) this.values.delete(key); } } async set(key: string, value: PasskeyChallengeRecord): Promise { this.pruneExpired(); this.values.set(key, { ...value }); } async consume(key: string): Promise { this.pruneExpired(); const value = this.values.get(key); this.values.delete(key); return value ? { ...value } : undefined; } } export function assertPasskeyProvider(provider: PasskeyProvider | undefined): PasskeyProvider { if (!provider) throw new Error("WRN-AUTH-PASSKEY-PROVIDER: configure a PasskeyProvider"); return provider; } export function publicKeyCreationOptions( options: PasskeyRegistrationOptions, ): PublicKeyCredentialCreationOptions { return { ...options, challenge: decode(options.challenge), user: { ...options.user, id: decode(options.user.id) }, excludeCredentials: options.excludeCredentials?.map((item) => ({ ...item, id: decode(item.id), transports: item.transports as AuthenticatorTransport[] | undefined, })), } as unknown as PublicKeyCredentialCreationOptions; } export function publicKeyRequestOptions( options: PasskeyAuthenticationOptions, ): PublicKeyCredentialRequestOptions { return { ...options, challenge: decode(options.challenge), allowCredentials: options.allowCredentials?.map((item) => ({ ...item, id: decode(item.id), transports: item.transports as AuthenticatorTransport[] | undefined, })), } as unknown as PublicKeyCredentialRequestOptions; } function decode(value: string): Uint8Array { if (!value || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) { throw new TypeError("Passkey data must be canonical Base64URL without padding"); } const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); const binary = atob(normalized + "=".repeat((4 - (normalized.length % 4)) % 4)); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); return bytes; }