import { fromBase64Url, toBase64Url } from "./base64url"; type Descriptor = { type: "public-key"; id: string }; type CreationOptions = { challenge: string; rp: { id: string; name: string }; user: { id: string; name: string; displayName: string }; pubKeyCredParams: { type: "public-key"; alg: number }[]; excludeCredentials: Descriptor[]; timeout: number; }; type RequestOptions = { challenge: string; rpId: string; allowCredentials: Descriptor[]; timeout: number }; function descriptors(list: Descriptor[]): PublicKeyCredentialDescriptor[] { return list.map((item) => ({ type: item.type, id: fromBase64Url(item.id) })); } /** Runs the browser registration ceremony and returns base64url fields for the server to verify. */ export async function createPasskey(options: CreationOptions) { const credential = await navigator.credentials.create({ publicKey: { challenge: fromBase64Url(options.challenge), rp: options.rp, user: { ...options.user, id: fromBase64Url(options.user.id) }, pubKeyCredParams: options.pubKeyCredParams, excludeCredentials: descriptors(options.excludeCredentials), authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" }, attestation: "none", timeout: options.timeout, }, }); if (!(credential instanceof PublicKeyCredential)) throw new Error("Passkey creation was cancelled"); const response = credential.response; if (!(response instanceof AuthenticatorAttestationResponse)) throw new Error("Unexpected passkey response"); return { challenge: options.challenge, clientDataJSON: toBase64Url(new Uint8Array(response.clientDataJSON)), attestationObject: toBase64Url(new Uint8Array(response.attestationObject)), transports: response.getTransports?.() ?? [], }; } export async function getPasskey(options: RequestOptions) { const credential = await navigator.credentials.get({ publicKey: { challenge: fromBase64Url(options.challenge), rpId: options.rpId, allowCredentials: descriptors(options.allowCredentials), userVerification: options.allowCredentials.length === 0 ? "required" : "preferred", timeout: options.timeout, }, }); if (!(credential instanceof PublicKeyCredential)) throw new Error("Passkey sign-in was cancelled"); const response = credential.response; if (!(response instanceof AuthenticatorAssertionResponse)) throw new Error("Unexpected passkey response"); return { challenge: options.challenge, credentialId: toBase64Url(new Uint8Array(credential.rawId)), clientDataJSON: toBase64Url(new Uint8Array(response.clientDataJSON)), authenticatorData: toBase64Url(new Uint8Array(response.authenticatorData)), signature: toBase64Url(new Uint8Array(response.signature)), }; }