// WebAuthn ceremony bridge: converts the JSON options from the server into the // ArrayBuffers navigator.credentials needs, runs the create/get ceremony, and posts // the (base64url-encoded) result back to the Livewire component. // Exposed as window.clusevWebauthn for the Settings + 2FA-challenge views. const b64uToBuf = (s) => { const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4)); const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad); const buf = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i); return buf.buffer; }; const bufToB64u = (buf) => { let bin = ''; const bytes = new Uint8Array(buf); for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }; // Map server creation options (base64url strings) → PublicKeyCredentialCreationOptions. const toCreateOptions = (o) => { const pk = { ...o }; pk.challenge = b64uToBuf(o.challenge); pk.user = { ...o.user, id: b64uToBuf(o.user.id) }; pk.excludeCredentials = (o.excludeCredentials || []).map((c) => ({ ...c, id: b64uToBuf(c.id) })); return pk; }; const toRequestOptions = (o) => { const pk = { ...o }; pk.challenge = b64uToBuf(o.challenge); pk.allowCredentials = (o.allowCredentials || []).map((c) => ({ ...c, id: b64uToBuf(c.id) })); return pk; }; const encodeAttestation = (cred) => ({ id: cred.id, type: cred.type, rawId: bufToB64u(cred.rawId), response: { clientDataJSON: bufToB64u(cred.response.clientDataJSON), attestationObject: bufToB64u(cred.response.attestationObject), }, clientExtensionResults: cred.getClientExtensionResults?.() ?? {}, }); const encodeAssertion = (cred) => ({ id: cred.id, type: cred.type, rawId: bufToB64u(cred.rawId), response: { clientDataJSON: bufToB64u(cred.response.clientDataJSON), authenticatorData: bufToB64u(cred.response.authenticatorData), signature: bufToB64u(cred.response.signature), userHandle: cred.response.userHandle ? bufToB64u(cred.response.userHandle) : null, }, clientExtensionResults: cred.getClientExtensionResults?.() ?? {}, }); window.clusevWebauthn = { async register($wire) { try { const options = await $wire.options(); const cred = await navigator.credentials.create({ publicKey: toCreateOptions(options) }); await $wire.register(encodeAttestation(cred)); } catch (e) { // user cancelled or the authenticator failed — surface nothing noisy console.warn('WebAuthn registration aborted', e); } }, async login($wire) { try { const options = await $wire.assertionOptions(); const cred = await navigator.credentials.get({ publicKey: toRequestOptions(options) }); await $wire.verifyWebauthn(encodeAssertion(cred)); } catch (e) { console.warn('WebAuthn login aborted', e); } }, };