UNPKG

accounts

Version:

Tempo Accounts SDK

212 lines (191 loc) 7.64 kB
import { Bytes } from 'ox' import type { Hex } from 'viem' import { Authentication, Registration } from 'webauthx/server' import * as Storage from './Storage.js' /** Pluggable strategy for WebAuthn registration and authentication ceremonies. */ export type WebAuthnCeremony = { /** Get credential creation options for `navigator.credentials.create()`. */ getRegistrationOptions: ( params: getRegistrationOptions.Parameters, ) => Promise<getRegistrationOptions.ReturnType> /** Verify a registration response and extract the public key. */ verifyRegistration: ( credential: Registration.Credential, options?: verifyRegistration.Options | undefined, ) => Promise<verifyRegistration.ReturnType> /** Get credential request options for `navigator.credentials.get()`. */ getAuthenticationOptions: ( params?: getAuthenticationOptions.Parameters | undefined, ) => Promise<getAuthenticationOptions.ReturnType> /** Verify an authentication response and extract the public key. */ verifyAuthentication: ( response: Authentication.Response, ) => Promise<verifyAuthentication.ReturnType> } export declare namespace getRegistrationOptions { type Parameters = { /** Credential IDs to exclude (prevents re-registering existing credentials). */ excludeCredentialIds?: readonly string[] | undefined /** Credential display name (e.g. `"alice"`). */ name: string /** Opaque user identifier. Encoded as `user.id` in the WebAuthn creation options. */ userId?: string | undefined } type ReturnType = { options: Registration.Options } } export declare namespace verifyRegistration { type Options = { /** Display name for the credential (e.g. user's email). */ name?: string | undefined } type ReturnType = { /** The registered credential's ID. */ credentialId: string /** The credential's public key (uncompressed P256, hex-encoded). */ publicKey: Hex /** Email associated with the account. */ email?: string | null | undefined /** Username associated with the account. */ username?: string | null | undefined } } export declare namespace getAuthenticationOptions { type Parameters = { /** Credential IDs to allow (restricts which credentials can be used). */ allowCredentialIds?: readonly string[] | undefined /** Challenge to use. */ challenge?: `0x${string}` | undefined /** Credential ID to restrict authentication to a specific credential. */ credentialId?: string | undefined /** Mediation hint for passkey autofill / conditional UI. */ mediation?: 'conditional' | 'optional' | 'required' | 'silent' | undefined } type ReturnType = { options: Authentication.Options } } export declare namespace verifyAuthentication { type ReturnType = { /** The authenticated credential's ID. */ credentialId: string /** The credential's public key (uncompressed P256, hex-encoded). */ publicKey: Hex /** User identifier from the authenticator's `userHandle` (discoverable/conditional flows). */ userId?: string | undefined /** Email associated with the account. */ email?: string | null | undefined /** Username associated with the account. */ username?: string | null | undefined } } /** Creates a {@link WebAuthnCeremony} from a custom implementation. */ export function from(ceremony: WebAuthnCeremony): WebAuthnCeremony { return ceremony } /** * Creates a pure client-side ceremony for development and prototyping. * * Generates challenges and verifies responses locally using `webauthx/server`. * Stores credentials in memory. No external server needed. * * @example * ```ts * import { WebAuthnCeremony } from 'accounts' * * const ceremony = WebAuthnCeremony.local() * ``` */ export function local(options: local.Options = {}): WebAuthnCeremony { const rpId = options.rpId ?? (typeof location !== 'undefined' ? location.hostname : 'localhost') const storage = options.storage ?? (typeof window !== 'undefined' ? Storage.idb() : Storage.memory()) const storageKey = 'credentials' return from({ async getRegistrationOptions(parameters) { const { excludeCredentialIds, name, userId } = parameters const { options } = Registration.getOptions({ excludeCredentialIds: excludeCredentialIds as string[] | undefined, name, rp: { id: rpId, name: rpId }, user: userId ? { id: Bytes.fromString(userId), name } : undefined, }) return { options } }, async verifyRegistration(credential) { const publicKey = credential.publicKey const credentials = (await storage.getItem<Record<string, Hex>>(storageKey)) ?? {} credentials[credential.id] = publicKey await storage.setItem(storageKey, credentials) return { credentialId: credential.id, publicKey } }, async getAuthenticationOptions(parameters = {}) { const { allowCredentialIds, challenge, credentialId } = parameters const { options } = Authentication.getOptions({ challenge, credentialId: (allowCredentialIds as string[] | undefined) ?? credentialId, rpId, }) return { options } }, async verifyAuthentication(response) { const credentials = (await storage.getItem<Record<string, Hex>>(storageKey)) ?? {} const publicKey = credentials[response.id] if (!publicKey) throw new Error(`Unknown credential: ${response.id}`) return { credentialId: response.id, publicKey } }, }) } export declare namespace local { type Options = { /** Relying Party ID (e.g. `"example.com"`). @default location.hostname */ rpId?: string | undefined /** Storage adapter for credential persistence. @default Storage.idb() in browser, Storage.memory() otherwise. */ storage?: Storage.Storage | undefined } } /** * Creates a server-backed ceremony that delegates to a remote {@link Handler.webAuthn} endpoint. * * All challenge generation, verification, and credential storage happen server-side. * The client uses `fetch()` to communicate with 4 POST endpoints derived from the base URL. * * @example * ```ts * import { WebAuthnCeremony } from 'accounts' * * const ceremony = WebAuthnCeremony.server({ url: 'https://example.com/webauthn' }) * ``` */ export function server(options: server.Options): WebAuthnCeremony { const { url } = options async function request<returnType>(path: string, body: unknown): Promise<returnType> { const response = await fetch(`${url}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const json = await response.json() if (!response.ok) throw new Error((json as { error?: string }).error ?? 'Request failed') return json as returnType } return from({ async getRegistrationOptions(parameters) { const { excludeCredentialIds, name, userId } = parameters return request('/register/options', { excludeCredentialIds, name, userId }) }, async verifyRegistration(credential) { return request('/register', credential) }, async getAuthenticationOptions(parameters = {}) { const { allowCredentialIds, challenge, credentialId, mediation } = parameters return request('/login/options', { allowCredentialIds, challenge, credentialId, mediation }) }, async verifyAuthentication(response) { return request('/login', response) }, }) } export declare namespace server { type Options = { /** Base URL of the WebAuthn handler (e.g. `"https://example.com/webauthn"`). */ url: string } }