UNPKG

accounts

Version:

Tempo Accounts SDK

120 lines 5.06 kB
import { PublicKey, Signature } from 'ox'; import { SignatureEnvelope } from 'ox/tempo'; import { Account } from 'viem/tempo'; import { Authentication, Registration } from 'webauthx/client'; import * as Adapter from '../Adapter.js'; import * as WebAuthnCeremony from '../WebAuthnCeremony.js'; import * as Rpc from '../zod/rpc.js'; import { local } from './local.js'; /** * Creates a WebAuthn adapter backed by real passkey ceremonies. * * Wraps the {@link local} adapter with WebAuthn registration and authentication flows, * using the provided {@link WebAuthnCeremony} for challenge generation and verification. * * @example * ```ts * import { webAuthn } from 'accounts' * * const provider = Provider.create({ * adapter: webAuthn(), * }) * ``` */ export function webAuthn(options = {}) { const { auth, authUrl, icon, name, rdns } = options; const url = (() => { if (auth) return typeof auth === 'string' ? auth : auth.url; return authUrl; })(); return Adapter.define({ icon, name, rdns }, (parameters) => { const { storage } = parameters; const ceremony = options.ceremony ?? (url ? WebAuthnCeremony.server({ url }) : WebAuthnCeremony.local({ storage })); const base = local({ async createAccount(parameters) { const { options } = await ceremony.getRegistrationOptions(parameters); const rpId = options.publicKey?.rp.id; if (!rpId) throw new Error('rpId is required'); const credential = await Registration.create({ options }); const { publicKey, email, username } = await ceremony.verifyRegistration(credential, { name: parameters.name, }); await storage.setItem('lastCredentialId', credential.id); const account = Account.fromWebAuthnP256({ id: credential.id, publicKey }); return { accounts: [ { address: account.address, label: parameters.name, keyType: 'webAuthn', credential: { id: credential.id, publicKey, rpId }, }, ], email, username, }; }, async loadAccounts(parameters = {}) { const { selectAccount, digest } = parameters; const credentialId = selectAccount ? undefined : (parameters?.credentialId ?? (await storage.getItem('lastCredentialId')) ?? undefined); const { options } = await ceremony.getAuthenticationOptions({ ...parameters, challenge: digest, credentialId, }); const rpId = options.publicKey?.rpId; if (!rpId) throw new Error('rpId is required'); const response = await Authentication.sign({ options }); const { publicKey, email, username } = await ceremony.verifyAuthentication(response); await storage.setItem('lastCredentialId', response.id); const account = Account.fromWebAuthnP256({ id: response.id, publicKey }, { rpId }); const signature = digest ? SignatureEnvelope.serialize({ metadata: response.metadata, publicKey: PublicKey.fromHex(publicKey), signature: Signature.from(response.signature), type: 'webAuthn', }, { magic: true }) : undefined; return { accounts: [ { address: account.address, keyType: 'webAuthn', credential: { id: response.id, publicKey, rpId }, }, ], email, signature, username, }; }, })(parameters); // When a server-backed ceremony is used, also revoke the // `Handler.webAuthn` session on disconnect — otherwise the // `accounts_webauthn` cookie persists past `wallet_disconnect` // and follow-up authenticated requests still succeed. const disconnect = url ? async () => { await fetch(`${url}/logout`, { method: 'POST', credentials: 'include', }).catch(() => { }); } : undefined; return { ...base, actions: { ...base.actions, ...(disconnect ? { disconnect } : {}) }, persistAccounts: true, }; }); } //# sourceMappingURL=webAuthn.js.map