UNPKG

accounts

Version:

Tempo Accounts SDK

167 lines (149 loc) 5.57 kB
import { PublicKey, Signature } from 'ox' import { SignatureEnvelope } from 'ox/tempo' import { Account } from 'viem/tempo' import { Authentication, Registration } from 'webauthx/client' import type { z } from 'zod' import type { OneOf } from '../../internal/types.js' 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: webAuthn.Options = {}): Adapter.Adapter { 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<string>('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, } }) } export declare namespace webAuthn { type Options = OneOf< | { /** Ceremony strategy for WebAuthn registration and authentication. @default WebAuthnCeremony.local() */ ceremony?: WebAuthnCeremony.WebAuthnCeremony | undefined } | { /** * Server Authentication endpoint for WebAuthn ceremonies (shorthand for * `WebAuthnCeremony.server({ url })`). Accepts the same shape as the * Provider `auth` capability — only the `url` field is consumed here; * other fields (`challenge`, `verify`, `logout`, `returnToken`) are * SIWE-only and ignored by the WebAuthn ceremony. */ auth?: z.input<typeof Rpc.wallet_connect.auth> | undefined /** @deprecated Use `auth` instead. */ authUrl?: string | undefined } > & { /** Data URI of the provider icon. @default Black 1×1 SVG. */ icon?: `data:image/${string}` | undefined /** Display name of the provider (e.g. `"My Wallet"`). @default "Injected Wallet" */ name?: string | undefined /** Reverse DNS identifier. @default `com.{lowercase name}` */ rdns?: string | undefined } }