accounts
Version:
Tempo Accounts SDK
121 lines (115 loc) • 4.59 kB
text/typescript
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'
)
}