build-container / build (push) Successful in 2m14s
A signed-in device creates a short-lived, single-use code in Settings
("Invite a device"); the new device enters it on the sign-in page to create
its own passkey. Codes are throttled like other secrets and cannot be used
by signed-out clients to create more codes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
37 lines
2.2 KiB
JavaScript
37 lines
2.2 KiB
JavaScript
// Shared passkey helpers (sign-in page + Settings).
|
|
const b64u = {
|
|
enc: buf => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
|
dec: s => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - s.length % 4) % 4)), c => c.charCodeAt(0)),
|
|
};
|
|
async function authPost(path, body, method = 'POST') {
|
|
const r = await fetch(path, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
|
|
const j = await r.json().catch(() => ({}));
|
|
if (!r.ok) throw new Error(j.error || 'Request failed (' + r.status + ')');
|
|
return j;
|
|
}
|
|
async function passkeyRegister(setupCode, name, inviteCode) {
|
|
const { cid, options } = await authPost('/api/auth/register/options', { setupCode, inviteCode });
|
|
options.challenge = b64u.dec(options.challenge);
|
|
options.user.id = b64u.dec(options.user.id);
|
|
(options.excludeCredentials || []).forEach(c => { c.id = b64u.dec(c.id); });
|
|
const cred = await navigator.credentials.create({ publicKey: options });
|
|
await authPost('/api/auth/register/verify', { cid, name, response: {
|
|
id: cred.id, rawId: b64u.enc(cred.rawId), type: cred.type,
|
|
response: { clientDataJSON: b64u.enc(cred.response.clientDataJSON), attestationObject: b64u.enc(cred.response.attestationObject),
|
|
transports: cred.response.getTransports ? cred.response.getTransports() : [] },
|
|
clientExtensionResults: cred.getClientExtensionResults(), authenticatorAttachment: cred.authenticatorAttachment,
|
|
} });
|
|
}
|
|
async function passkeyLogin() {
|
|
const { cid, options } = await authPost('/api/auth/login/options');
|
|
options.challenge = b64u.dec(options.challenge);
|
|
(options.allowCredentials || []).forEach(c => { c.id = b64u.dec(c.id); });
|
|
const a = await navigator.credentials.get({ publicKey: options });
|
|
await authPost('/api/auth/login/verify', { cid, response: {
|
|
id: a.id, rawId: b64u.enc(a.rawId), type: a.type,
|
|
response: { clientDataJSON: b64u.enc(a.response.clientDataJSON), authenticatorData: b64u.enc(a.response.authenticatorData),
|
|
signature: b64u.enc(a.response.signature), userHandle: a.response.userHandle ? b64u.enc(a.response.userHandle) : undefined },
|
|
clientExtensionResults: a.getClientExtensionResults(),
|
|
} });
|
|
}
|