UNPKG

accounts

Version:

Tempo Accounts SDK

121 lines (115 loc) 4.59 kB
import { PublicKey, WebCryptoP256 } from 'ox' import { Account as TempoAccount } from 'viem/tempo' import { KeyUnavailableError, type Keystore } from '../Keystore.js' /** Handle persisted by the {@link webCryptoP256} keystore. */ type WebCryptoP256Handle = { kind: 'webcrypto-p256' } & ( | { /** Live key pair (non-extractable). Requires structured-clone storage. */ keyPair: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>> jwk?: undefined } | { /** Exported private key. At-rest protection is the host app's storage adapter. */ jwk: JsonWebKey keyPair?: undefined } ) /** * WebCrypto P-256 keystore. The built-in default (see `Keystore.defaults`). * * By default the key is generated non-extractable: the private key never has * a JS-visible encoding, and the handle holds the live `CryptoKey`, which * persists only through structured-clone storage (`Storage.idb`, * `Storage.memory`) — on string-based storage the key is session-only. * * Pass `extractable: true` for environments without structured-clone storage * where keys must survive restarts (e.g. React Native, Node, CLI): the * private key is exported once as a JWK (at-rest protection is the host * app's storage adapter) and re-imported non-extractable each session. In * browsers with IndexedDB storage, prefer the non-extractable default. * * @example * ```ts * import { Keystore, Provider } from 'accounts' * * const provider = Provider.create({ * accessKey: { keystores: { p256: Keystore.webCryptoP256({ extractable: true }) } }, * }) * ``` */ export function webCryptoP256(options: webCryptoP256.Options = {}): Keystore { const { extractable = false } = options return { requiresStructuredClone: !extractable, async createKey() { if (!globalThis.crypto?.subtle) throw new Error('`webCryptoP256` keystore requires WebCrypto (`crypto.subtle`) support.') const keyPair = await WebCryptoP256.createKeyPair({ extractable }) const publicKey = PublicKey.toHex(keyPair.publicKey) if (!extractable) return { handle: { keyPair, kind: 'webcrypto-p256' } satisfies WebCryptoP256Handle, publicKey, } const jwk = await globalThis.crypto.subtle.exportKey('jwk', keyPair.privateKey) return { handle: { jwk, kind: 'webcrypto-p256' } satisfies WebCryptoP256Handle, publicKey } }, async toAccount(record, context) { const handle = record.handle as Partial<WebCryptoP256Handle> | undefined if (handle?.kind !== 'webcrypto-p256') throw new Error('Unrecognized `webCryptoP256` keystore handle.') const account = { access: context.access, keyAuthorizationManager: context.keyAuthorizationManager, } if (handle.keyPair) { // A live key pair serialized through non-structured-clone storage // arrives mangled — the key is permanently gone. if (!isCryptoKey(handle.keyPair.privateKey)) throw new KeyUnavailableError('`webCryptoP256` key material did not survive storage.') return TempoAccount.fromWebCryptoP256(handle.keyPair, account) } if (handle.jwk) { const privateKey = await globalThis.crypto.subtle .importKey('jwk', handle.jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']) .catch((error) => { throw new KeyUnavailableError( '`webCryptoP256` keystore handle holds unusable key material.', { cause: error }, ) }) return TempoAccount.fromWebCryptoP256( { privateKey, publicKey: PublicKey.fromHex(record.publicKey) }, account, ) } throw new Error('Unrecognized `webCryptoP256` keystore handle.') }, } } export declare namespace webCryptoP256 { /** Options for {@link webCryptoP256}. */ type Options = { /** * Generate the key extractable and persist it as a JWK so it survives * string-based storage adapters. When `false` (default), the key is * non-extractable from creation and persists only through * structured-clone storage. * * @default false */ extractable?: boolean | undefined } } function isCryptoKey(value: unknown): value is CryptoKey { if (typeof CryptoKey !== 'undefined' && value instanceof CryptoKey) return true // Cross-realm fallback (e.g. keys structured-cloned through IndexedDB). return ( !!value && typeof value === 'object' && 'algorithm' in value && (value as CryptoKey).type === 'private' ) }