rspace-online/shared/x402/passkey-signer.ts

143 lines
4.4 KiB
TypeScript

/**
* Passkey-backed x402 Signer
*
* Client-side module that uses the EncryptID passkey (WebAuthn PRF) to
* derive an EOA and sign x402 micropayments. Replaces the EVM_PRIVATE_KEY
* env var approach with hardware-backed passkey signing.
*
* Flow:
* 1. Authenticate with passkey (gets PRF output)
* 2. Derive EOA via deriveEOAFromPRF()
* 3. Create x402 client with EOA as signer
* 4. Return wrapped fetch that auto-handles 402 responses
* 5. Zero the private key bytes after signing
*/
import { deriveEOAFromPRF, zeroPrivateKey } from '../../src/encryptid/eoa-derivation';
import type { DerivedKeys } from '../../src/encryptid/key-derivation';
/**
* Options for creating a passkey-backed x402 fetch wrapper.
*/
export interface PasskeySignerOptions {
/** Network identifier (default: 'eip155:84532' for Base Sepolia) */
network?: string;
/** PRF output from passkey authentication */
prfOutput: Uint8Array;
}
/**
* Result from creating a passkey signer, including cleanup.
*/
export interface PasskeySignerResult {
/** The fetch function with x402 payment handling */
paidFetch: typeof fetch;
/** The derived EOA address */
eoaAddress: string;
/** Call this to zero out the private key from memory */
cleanup: () => void;
}
/**
* Create a passkey-backed x402 payment signer.
*
* Derives an EOA from the passkey PRF output and creates a fetch wrapper
* that automatically handles 402 Payment Required responses by signing
* USDC transfers on the specified network.
*
* @example
* ```ts
* const { paidFetch, eoaAddress, cleanup } = await createPasskeySigner({
* prfOutput: prfOutputBytes,
* network: 'eip155:84532',
* });
*
* try {
* const res = await paidFetch('https://example.com/paid-endpoint', {
* method: 'POST',
* body: JSON.stringify({ data: 'test' }),
* });
* console.log('Payment + response:', await res.json());
* } finally {
* cleanup(); // Zero private key
* }
* ```
*/
export async function createPasskeySigner(
options: PasskeySignerOptions,
): Promise<PasskeySignerResult> {
const network = options.network || 'eip155:84532';
// Derive EOA from PRF output
const eoa = deriveEOAFromPRF(options.prfOutput);
// Dynamic imports for x402 client libraries (only loaded when needed)
const [{ x402Client, wrapFetchWithPayment }, { ExactEvmScheme }, { privateKeyToAccount }] =
await Promise.all([
import('@x402/fetch'),
import('@x402/evm/exact/client'),
import('viem/accounts'),
]);
// Convert raw private key bytes to viem account
const hexKey = ('0x' + Array.from(eoa.privateKey).map(b => b.toString(16).padStart(2, '0')).join('')) as `0x${string}`;
const account = privateKeyToAccount(hexKey);
// Set up x402 client with the derived EOA
// Type cast needed: viem's LocalAccount doesn't include readContract,
// but ExactEvmScheme only uses signing methods at runtime
const client = new x402Client();
client.register(network as `${string}:${string}`, new ExactEvmScheme(account as any));
const paidFetch = wrapFetchWithPayment(fetch as any, client) as unknown as typeof fetch;
return {
paidFetch,
eoaAddress: eoa.address,
cleanup: () => {
zeroPrivateKey(eoa.privateKey);
},
};
}
/**
* Create a passkey signer directly from EncryptID DerivedKeys.
*
* Convenience wrapper when keys are already available from the key manager.
*/
export async function createPasskeySignerFromKeys(
keys: DerivedKeys,
network?: string,
): Promise<PasskeySignerResult | null> {
if (!keys.eoaPrivateKey || !keys.eoaAddress) {
return null;
}
const net = network || 'eip155:84532';
const [{ x402Client, wrapFetchWithPayment }, { ExactEvmScheme }, { privateKeyToAccount }] =
await Promise.all([
import('@x402/fetch'),
import('@x402/evm/exact/client'),
import('viem/accounts'),
]);
const hexKey = ('0x' + Array.from(keys.eoaPrivateKey).map(b => b.toString(16).padStart(2, '0')).join('')) as `0x${string}`;
const account = privateKeyToAccount(hexKey);
const client = new x402Client();
client.register(net as `${string}:${string}`, new ExactEvmScheme(account as any));
const paidFetch = wrapFetchWithPayment(fetch as any, client) as unknown as typeof fetch;
return {
paidFetch,
eoaAddress: keys.eoaAddress,
cleanup: () => {
if (keys.eoaPrivateKey) {
zeroPrivateKey(keys.eoaPrivateKey);
}
},
};
}