import { callWorker } from './workerClient'; import type { AccountStatus } from './account'; import type { PasskeyOptionsBody, PasskeyInfoBody } from './generated/api'; const apiBase = import.meta.env.VITE_WEBDROP_API_URL ?? '/api/v1'; const call = (operation: string, payload: object = {}) => callWorker(operation, { ...payload, apiBase }); export const passkeysAvailable = () => typeof PublicKeyCredential !== 'undefined' && window.isSecureContext; const decode = (value: string): Uint8Array => Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=')), c => c.charCodeAt(0)); interface PrfResult { enabled?: boolean; results?: { first?: ArrayBuffer } } const prfResult = (credential: PublicKeyCredential) => (credential.getClientExtensionResults() as AuthenticationExtensionsClientOutputs & { prf?: PrfResult }).prf; function credentialJson(credential: PublicKeyCredential): string { // W3C PRF: omit client extension outputs when sending credentials to the server. return JSON.stringify({ ...credential.toJSON(), clientExtensionResults: {} }); } async function assertion(options: PasskeyOptionsBody, credentialId?: string): Promise { const json = JSON.parse(options.options) as PublicKeyCredentialRequestOptionsJSON; const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(json); if (credentialId) publicKey.allowCredentials = [{ type: 'public-key', id: decode(credentialId) }]; const evalByCredential = Object.fromEntries(options.credentials.filter(p => !credentialId || p.id === credentialId).map(p => [p.id, { first: decode(p.prf_salt) }])); publicKey.extensions = { ...publicKey.extensions, prf: { evalByCredential } } as AuthenticationExtensionsClientInputs; const credential = await navigator.credentials.get({ publicKey }); if (!(credential instanceof PublicKeyCredential)) throw new Error('Passkey验证未完成'); return credential; } export async function bindPasskey(password: string, name: string): Promise { if (!passkeysAvailable()) throw new Error('当前浏览器不支持Passkey,请使用HTTPS或localhost'); let prf: ArrayBuffer | undefined; try { const options = await call('beginPasskeyRegistration', { password }); const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(options.options)); publicKey.extensions = { ...publicKey.extensions, prf: { eval: { first: decode(options.prf_salt!) } } } as AuthenticationExtensionsClientInputs; const credential = await navigator.credentials.create({ publicKey }); if (!(credential instanceof PublicKeyCredential)) throw new Error('Passkey绑定未完成'); const extension = prfResult(credential); prf = extension?.results?.first; const registered = await call('finishPasskeyRegistration', { id: options.id, credentialId: credential.id, credential: credentialJson(credential), name, prf }); if (!registered.can_recover && extension?.enabled) { const request = await call('beginPasskeyRecovery'); const proof = await assertion(request, registered.id); prf = prfResult(proof)?.results?.first; if (prf) await call('finishPasskeyRecovery', { id: request.id, credentialId: proof.id, credential: credentialJson(proof), prf }); } } finally { if (prf) new Uint8Array(prf).fill(0); await call('cancelPasskey').catch(() => {}); } } export async function loginWithPasskey(username: string, remember: boolean): Promise { if (!passkeysAvailable()) throw new Error('当前浏览器不支持Passkey,请使用HTTPS或localhost'); const options = await call('beginPasskeyLogin', { username }); const proof = await assertion(options); const prf = prfResult(proof)?.results?.first; try { return await call('finishPasskeyLogin', { username, id: options.id, credential: credentialJson(proof), prf, remember }); } finally { if (prf) new Uint8Array(prf).fill(0); } } export const passkeyApi = { list: () => call('listPasskeys'), remove: (id: string) => call('removePasskey', { id }), unlock: (password: string, remember: boolean) => call('unlockPasskeyLogin', { password, remember }), cancel: () => call('cancelPasskey'), };