// WebAuthn helpers — base64url encoding compatible with Fido2NetLib function base64urlToBuffer(base64url) { const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/'); const binary = atob(base64); return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer; } function bufferToBase64url(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (const b of bytes) binary += String.fromCharCode(b); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } function prepareCreationOptions(options) { options.challenge = base64urlToBuffer(options.challenge); options.user.id = base64urlToBuffer(options.user.id); if (options.excludeCredentials) { options.excludeCredentials = options.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id), })); } return options; } function prepareRequestOptions(options) { options.challenge = base64urlToBuffer(options.challenge); if (options.allowCredentials) { options.allowCredentials = options.allowCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id), })); } return options; } function serializeAttestation(credential) { return { id: credential.id, rawId: bufferToBase64url(credential.rawId), type: credential.type, response: { attestationObject: bufferToBase64url(credential.response.attestationObject), clientDataJSON: bufferToBase64url(credential.response.clientDataJSON), }, extensions: credential.getClientExtensionResults(), }; } function serializeAssertion(credential) { return { id: credential.id, rawId: bufferToBase64url(credential.rawId), type: credential.type, response: { authenticatorData: bufferToBase64url(credential.response.authenticatorData), clientDataJSON: bufferToBase64url(credential.response.clientDataJSON), signature: bufferToBase64url(credential.response.signature), userHandle: credential.response.userHandle ? bufferToBase64url(credential.response.userHandle) : null, }, extensions: credential.getClientExtensionResults(), }; } window.passkeyEnroll = async function () { const optRes = await fetch('/api/auth/register/options', { method: 'POST' }); if (!optRes.ok) throw new Error(await optRes.text()); const options = prepareCreationOptions(await optRes.json()); const credential = await navigator.credentials.create({ publicKey: options }); const completeRes = await fetch('/api/auth/register/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(serializeAttestation(credential)), }); if (!completeRes.ok) throw new Error(await completeRes.text()); return await completeRes.json(); }; window.passkeyLogin = async function () { const optRes = await fetch('/api/auth/login/options', { method: 'POST' }); if (!optRes.ok) throw new Error(await optRes.text()); const options = prepareRequestOptions(await optRes.json()); const credential = await navigator.credentials.get({ publicKey: options }); const completeRes = await fetch('/api/auth/login/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(serializeAssertion(credential)), }); if (!completeRes.ok) throw new Error(await completeRes.text()); return await completeRes.json(); };