UNPKG

accounts

Version:

Tempo Accounts SDK

137 lines (127 loc) 5.05 kB
import { spawn } from 'node:child_process' import { KeyAuthorization } from 'ox/tempo' import type { DeviceCode } from 'wata' import * as z from 'zod/mini' import type * as Adapter from '../core/Adapter.js' import { deviceCode } from '../core/adapters/deviceCode/deviceCode.js' import * as Keystore from '../core/Keystore.js' import * as Rpc from '../core/zod/rpc.js' /** * Creates a CLI bootstrap adapter backed by the Wata device-code transport * (OAuth 2.0 Device Authorization Grant, RFC 8628 with PKCE). * * Authorization opens the wallet's verification page in a browser; the wallet * responds with a signed key authorization once the user approves. */ export function cli(options: cli.Options): Adapter.Adapter { const { host, name = 'Tempo CLI', open = defaultOpen, pollingInterval, rdns = 'xyz.tempo.cli', timeout, } = options return deviceCode({ actions: ({ getClient, request, store }) => { return { // TODO: Move pending access-key replacement into the shared provider once // all remote adapters support the extended request and response. // Limit updates run their own ceremony: the wallet returns a // replacement signed authorization for a pending (unpublished) key, // or applies the update on-chain for a published one. async updateAccessKey(parameters) { const chainId = Number(parameters.chainId ?? store.getState().chainId) const current = store.accessKeys.list({ accessKey: parameters.accessKeyAddress, account: parameters.address, chainId, })[0] const status = current?.keyAuthorization ? await store.accessKeys.getStatus({ accessKey: parameters.accessKeyAddress, account: parameters.address, chainId, client: getClient({ chainId }), }) : undefined const pendingKeyAuthorization = status === 'pending' && current?.keyAuthorization ? KeyAuthorization.toRpc(current.keyAuthorization) : undefined const encoded = z.encode(Rpc.wallet_updateAccessKey.parameters, { ...parameters, chainId: BigInt(chainId), }) const result = (await request({ context: { account: parameters.address, chainId }, method: 'wallet_updateAccessKey', params: [ { ...encoded, ...(pendingKeyAuthorization ? { keyAuthorization: pendingKeyAuthorization } : {}), }, ], })) as { keyAuthorization?: KeyAuthorization.Rpc | undefined } | null | undefined if (result?.keyAuthorization) store.accessKeys.updateAuthorization({ accessKey: parameters.accessKeyAddress, account: parameters.address, authorization: KeyAuthorization.fromRpc(result.keyAuthorization), chainId, }) }, } }, // The CLI persists to string-based filesystem storage, so its p256 // default opts into an extractable WebCrypto key (a non-extractable one // could not survive a restart). secp256k1 stays available for explicit // requests. keystores: { p256: Keystore.webCryptoP256({ extractable: true }), secp256k1: Keystore.secp256k1(), }, meta: { name }, name, async onPrompt(prompt) { if (!prompt.verificationUriFull) { const code = prompt.userCode.replace(/^(.{4})(.{4})$/, '$1-$2') process.stdout.write(`Enter code ${code} at ${prompt.verificationUri}\n`) } await open(prompt.verificationUriFull ?? prompt.verificationUri, prompt) }, rdns, ...(pollingInterval !== undefined ? { pollingInterval } : {}), ...(timeout !== undefined ? { timeout } : {}), url: host, }) } export declare namespace cli { export type Options = { /** Base URL of the wallet's device-code endpoints (derives `/register` and `/token`). */ host: string /** Provider display name. @default "Tempo CLI" */ name?: string | undefined /** Browser opener override. */ open?: ((url: string, prompt: DeviceCode.Prompt) => Promise<void> | void) | undefined /** Poll cadence override in milliseconds. Defaults to the host-advertised interval. */ pollingInterval?: number | undefined /** Reverse-DNS provider identifier. @default "xyz.tempo.cli" */ rdns?: string | undefined /** Overall wait for user approval in milliseconds. @default 300000 */ timeout?: number | undefined } } function defaultOpen(url: string) { const command = process.platform === 'darwin' ? { command: 'open', args: [url] } : process.platform === 'win32' ? { command: 'cmd', args: ['/c', 'start', '', url] } : { command: 'xdg-open', args: [url] } const child = spawn(command.command, command.args, { detached: true, stdio: 'ignore', }) child.unref() }