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)),