@keeperhub/wallet 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +0 -74
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +0 -74
- package/dist/cli.js.map +1 -1
- package/dist/hook-entrypoint.cjs +0 -185
- package/dist/hook-entrypoint.cjs.map +1 -1
- package/dist/hook-entrypoint.js +0 -185
- package/dist/hook-entrypoint.js.map +1 -1
- package/dist/index.cjs +57 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -15
- package/dist/index.d.ts +18 -15
- package/dist/index.js +57 -119
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/keeperhub-wallet.skill.md +25 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hmac.ts","../src/types.ts","../src/client.ts","../src/safety-config.ts","../src/storage.ts","../src/hook.ts","../src/hook-entrypoint.ts"],"sourcesContent":["import { createHash, createHmac } from \"node:crypto\";\nimport type { HmacHeaders } from \"./types.js\";\n\n/**\n * Mirror of lib/agentic-wallet/hmac.ts::computeSignature.\n * Format (byte-for-byte identical to server):\n * `${method}\\n${path}\\n${subOrgId}\\n${sha256_hex(body)}\\n${timestamp}`\n * Post-HI-05: subOrgId is a signed field.\n *\n * @security Do NOT log the secret or the returned signature. Any stdout\n * emitter (the global console object or util.inspect) added to this\n * file is a T-34-08 violation (grep-enforced).\n */\nexport function computeSignature(\n secret: string,\n method: string,\n path: string,\n subOrgId: string,\n body: string,\n timestamp: string\n): string {\n const bodyDigest = createHash(\"sha256\").update(body).digest(\"hex\");\n const signingString = `${method}\\n${path}\\n${subOrgId}\\n${bodyDigest}\\n${timestamp}`;\n return createHmac(\"sha256\", secret).update(signingString).digest(\"hex\");\n}\n\n/**\n * Build the three X-KH-* headers that authenticate every request to\n * /api/agentic-wallet/* (except /provision, which uses the session cookie).\n *\n * Timestamp is unix seconds (Math.floor(Date.now() / 1000)); the server\n * enforces a symmetric 300-second replay window.\n */\nexport function buildHmacHeaders(\n secret: string,\n method: string,\n path: string,\n subOrgId: string,\n body: string\n): HmacHeaders {\n const timestamp = String(Math.floor(Date.now() / 1000));\n const signature = computeSignature(\n secret,\n method,\n path,\n subOrgId,\n body,\n timestamp\n );\n return {\n \"X-KH-Sub-Org\": subOrgId,\n \"X-KH-Timestamp\": timestamp,\n \"X-KH-Signature\": signature,\n };\n}\n","// Shared types across the package. Phase 34.\nexport type WalletConfig = {\n /** Turnkey sub-org ID returned by POST /api/agentic-wallet/provision */\n subOrgId: string;\n /** EVM-shared wallet address (same for Base chainId 8453 and Tempo chainId 4217) */\n walletAddress: `0x${string}`;\n /** 64-char lowercase hex HMAC secret, minted server-side at provision; never logged */\n hmacSecret: string;\n};\n\nexport type HmacHeaders = {\n \"X-KH-Sub-Org\": string;\n \"X-KH-Timestamp\": string;\n \"X-KH-Signature\": string;\n};\n\nexport type HookDecision = {\n decision: \"allow\" | \"deny\" | \"ask\";\n reason?: string;\n};\n\nexport class KeeperHubError extends Error {\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(message);\n this.name = \"KeeperHubError\";\n this.code = code;\n }\n}\n\nexport class WalletConfigMissingError extends Error {\n constructor() {\n super(\n \"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision.\"\n );\n this.name = \"WalletConfigMissingError\";\n }\n}\n","import { buildHmacHeaders } from \"./hmac.js\";\nimport { KeeperHubError, type WalletConfig } from \"./types.js\";\n\nexport type ClientOptions = {\n /** Defaults to process.env.KEEPERHUB_API_URL ?? \"https://app.keeperhub.com\" */\n baseUrl?: string;\n /** Injected for tests; defaults to global fetch */\n fetch?: typeof fetch;\n};\n\n/**\n * 202 ask-tier envelope returned by /sign and /approval-request when the\n * risk classifier routes a request to the ask queue. Callers poll\n * `/api/agentic-wallet/approval-request/:id` until status !== \"pending\".\n */\nexport type AskTierResponse = {\n _status: 202;\n approvalRequestId: string;\n};\n\nconst TRAILING_SLASH = /\\/$/;\n\nfunction defaultCodeForStatus(status: number): string {\n if (status === 401) {\n return \"HMAC_INVALID\";\n }\n if (status === 403) {\n return \"POLICY_BLOCKED\";\n }\n if (status === 404) {\n return \"NOT_FOUND\";\n }\n if (status === 502) {\n return \"TURNKEY_UPSTREAM\";\n }\n return `HTTP_${status}`;\n}\n\n/**\n * HMAC-signed HTTP client for the KeeperHub agentic-wallet API surface.\n * Every request to /api/agentic-wallet/* (except /provision, which uses\n * the session cookie) flows through this class.\n *\n * @security No logging of headers, body, or response bodies. Any stdout\n * emitter (the global console object or util.inspect) added to this\n * file is a T-34-08 violation (grep-enforced in CI).\n */\nexport class KeeperHubClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly wallet: WalletConfig;\n\n constructor(wallet: WalletConfig, opts: ClientOptions = {}) {\n this.wallet = wallet;\n const envBase = process.env.KEEPERHUB_API_URL;\n this.baseUrl = (\n opts.baseUrl ??\n envBase ??\n \"https://app.keeperhub.com\"\n ).replace(TRAILING_SLASH, \"\");\n this.fetchImpl = opts.fetch ?? globalThis.fetch;\n }\n\n /**\n * HMAC-signed POST/GET to any /api/agentic-wallet/* route except\n * /provision. Path MUST start with a leading slash. Body is\n * JSON.stringify'd (or the empty string for GET).\n *\n * Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,\n * message)` where `code` is the server-supplied field or the default\n * taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,\n * `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an\n * AskTierResponse envelope.\n */\n async request<T>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown\n ): Promise<T | AskTierResponse> {\n const bodyStr = body === undefined ? \"\" : JSON.stringify(body);\n const hmacHeaders = buildHmacHeaders(\n this.wallet.hmacSecret,\n method,\n path,\n this.wallet.subOrgId,\n bodyStr\n );\n const headers: Record<string, string> =\n method === \"POST\"\n ? { ...hmacHeaders, \"content-type\": \"application/json\" }\n : { ...hmacHeaders };\n const response = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: method === \"POST\" ? bodyStr : undefined,\n });\n\n if (response.status === 202) {\n const data = (await response.json()) as {\n approvalRequestId: string;\n status: string;\n };\n return { _status: 202, approvalRequestId: data.approvalRequestId };\n }\n\n if (!response.ok) {\n let code = \"UNKNOWN\";\n let message = `HTTP ${response.status}`;\n try {\n const data = (await response.json()) as {\n code?: string;\n error?: string;\n };\n code = data.code ?? defaultCodeForStatus(response.status);\n message = data.error ?? message;\n } catch {\n // body is not JSON -- keep the default code + message\n }\n throw new KeeperHubError(code, message);\n }\n\n return (await response.json()) as T;\n }\n}\n","import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * User-owned safety config at ~/.keeperhub/safety.json. File mode 0o644 so the\n * user can freely edit thresholds and the allowlist; server-side Turnkey policy\n * remains the authoritative hard cap (GUARD-06).\n */\nexport type SafetyConfig = {\n auto_approve_max_usd: number;\n ask_threshold_usd: number;\n block_threshold_usd: number;\n allowlisted_contracts: string[];\n};\n\n/**\n * Defaults per 34-CONTEXT lines 61-68. Thresholds bracket the Turnkey policy\n * hard cap (100 USDC). Allowlisted contracts mirror the server Turnkey policy\n * allowlist (lib/agentic-wallet/policy.ts FACILITATOR_ALLOWLIST) -- lowercased\n * for case-insensitive match against tool_input.to / paymentChallenge.payTo.\n */\nexport const DEFAULT_SAFETY_CONFIG: SafetyConfig = {\n auto_approve_max_usd: 5,\n ask_threshold_usd: 50,\n block_threshold_usd: 100,\n allowlisted_contracts: [\n \"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\", // Base USDC\n \"0x20c000000000000000000000b9537d11c60e8b50\", // Tempo USDC.e\n ],\n};\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"safety.json\")`\n// itself -- matches storage.ts. Hoisting to a module-level constant would\n// freeze $HOME at import time and break tests that override process.env.HOME\n// in beforeEach.\n\nfunction getSafetyPath(): string {\n return join(homedir(), \".keeperhub\", \"safety.json\");\n}\n\nexport async function loadSafetyConfig(): Promise<SafetyConfig> {\n const path = getSafetyPath();\n let raw: string;\n try {\n raw = await readFile(path, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n await mkdir(dirname(path), { recursive: true, mode: 0o700 });\n await writeFile(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {\n mode: 0o644,\n });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(path, 0o644);\n return DEFAULT_SAFETY_CONFIG;\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<SafetyConfig>;\n return validateAndMerge(parsed);\n}\n\nconst THRESHOLD_KEYS = [\n \"auto_approve_max_usd\",\n \"ask_threshold_usd\",\n \"block_threshold_usd\",\n] as const;\n\nexport function validateAndMerge(partial: Partial<SafetyConfig>): SafetyConfig {\n const merged: SafetyConfig = {\n auto_approve_max_usd:\n partial.auto_approve_max_usd ??\n DEFAULT_SAFETY_CONFIG.auto_approve_max_usd,\n ask_threshold_usd:\n partial.ask_threshold_usd ?? DEFAULT_SAFETY_CONFIG.ask_threshold_usd,\n block_threshold_usd:\n partial.block_threshold_usd ?? DEFAULT_SAFETY_CONFIG.block_threshold_usd,\n allowlisted_contracts:\n partial.allowlisted_contracts ??\n DEFAULT_SAFETY_CONFIG.allowlisted_contracts,\n };\n\n for (const key of THRESHOLD_KEYS) {\n const v = merged[key];\n if (!(Number.isFinite(v) && v >= 0)) {\n throw new Error(\n `safety.json: ${key} must be a non-negative finite number; got ${String(v)}`\n );\n }\n }\n if (merged.ask_threshold_usd < merged.auto_approve_max_usd) {\n throw new Error(\n \"safety.json: ask_threshold_usd must be >= auto_approve_max_usd\"\n );\n }\n if (merged.block_threshold_usd < merged.ask_threshold_usd) {\n throw new Error(\n \"safety.json: block_threshold_usd must be >= ask_threshold_usd\"\n );\n }\n if (!Array.isArray(merged.allowlisted_contracts)) {\n throw new Error(\"safety.json: allowlisted_contracts must be an array\");\n }\n merged.allowlisted_contracts = merged.allowlisted_contracts.map((a) =>\n a.toLowerCase()\n );\n return merged;\n}\n\nexport function getSafetyConfigPath(): string {\n return getSafetyPath();\n}\n","import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { type WalletConfig, WalletConfigMissingError } from \"./types.js\";\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"wallet.json\")`\n// itself. Do NOT hoist to a module-level `const WALLET_PATH` -- tests\n// override `process.env.HOME` in `beforeEach` and `homedir()` must re-read\n// that on each call. A hoisted constant would freeze the harness's original\n// HOME at import time and every test would write into the real\n// ~/.keeperhub/ directory.\n\nexport async function readWalletConfig(): Promise<WalletConfig> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n let raw: string;\n try {\n raw = await readFile(walletPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new WalletConfigMissingError();\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<WalletConfig>;\n if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {\n throw new Error(`Malformed wallet.json at ${walletPath}`);\n }\n return parsed as WalletConfig;\n}\n\nexport async function writeWalletConfig(config: WalletConfig): Promise<void> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n await mkdir(dirname(walletPath), { recursive: true, mode: 0o700 });\n await writeFile(walletPath, JSON.stringify(config, null, 2), { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(walletPath, 0o600);\n}\n\nexport function getWalletConfigPath(): string {\n return join(homedir(), \".keeperhub\", \"wallet.json\");\n}\n","import { KeeperHubClient } from \"./client.js\";\nimport { loadSafetyConfig, type SafetyConfig } from \"./safety-config.js\";\nimport { readWalletConfig } from \"./storage.js\";\nimport type { HookDecision, WalletConfig } from \"./types.js\";\n\ntype HookInput = {\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n};\n\nexport type CreateHookOptions = {\n /** Match against tool_name. Default: /keeperhub|wallet|sign/i */\n toolNameMatcher?: (name: string) => boolean;\n /** Injected for tests */\n walletLoader?: () => Promise<WalletConfig>;\n /** Injected for tests */\n configLoader?: () => Promise<SafetyConfig>;\n /** Injected for tests */\n clientFactory?: (w: WalletConfig) => KeeperHubClient;\n /**\n * Called when the ask tier opens an approval URL. Default: write to stderr\n * (stdout is reserved for the Claude Code hook JSON output).\n */\n onAskOpen?: (url: string) => void;\n /** Polling config for the ask tier */\n poll?: { intervalMs: number; maxAttempts: number };\n};\n\nconst DEFAULT_POLL = { intervalMs: 2000, maxAttempts: 150 } as const;\nconst APPROVAL_URL_BASE = \"https://app.keeperhub.com/approve/\";\nconst USDC_DECIMALS = 1_000_000;\nconst ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\nconst MICRO_USDC_RE = /^\\d+$/;\nconst DEFAULT_TOOL_RE = /keeperhub|wallet|sign/i;\n\nfunction defaultToolMatcher(name: string): boolean {\n return DEFAULT_TOOL_RE.test(name);\n}\n\n/**\n * Coerce an amount field to micro-USDC. Inputs MUST be explicitly tagged with\n * `unit`:\n * - `{amount: string, unit: \"microUsdc\"}` -> parsed as integer micro-USDC\n * (x402 wire format)\n * - `{amount: number, unit: \"usd\"}` -> multiplied by 1_000_000\n *\n * Untagged amounts are REJECTED with a thrown TypeError. This is GUARD-05:\n * we refuse to guess whether a \"5\" is 5 USD or 5 micro-USDC (a six-order-of-\n * magnitude reading error). The caller must commit.\n *\n * Fields read: ONLY tool_input.paymentChallenge.{amount,unit} and\n * tool_input.{amount,unit}. Forged safety-bypass fields (any \"trust-level\"\n * hint, \"is-safe\" boolean, \"admin-override\" bit, or similar) are NEVER read;\n * thresholds come exclusively from ~/.keeperhub/safety.json.\n */\nfunction extractAmountMicroUsdc(input: HookInput): bigint | null {\n const ti = input.tool_input ?? {};\n const challenge = (ti.paymentChallenge ?? {}) as Record<string, unknown>;\n // WR-01: prefer the signed wire field (paymentChallenge.amount/unit) over\n // caller-supplied sibling tool_input fields. The nested challenge is the\n // field the downstream /sign call actually binds into the signed bytes, so\n // a misbehaving tool cannot slip a larger nested amount past the auto cap\n // by shadowing it with a small top-level sibling. Fall back to top-level\n // only when no challenge is present (e.g. direct /sign tool calls with no\n // 402 round).\n const directAmount = challenge.amount ?? ti.amount;\n const directUnit = challenge.unit ?? ti.unit;\n\n if (directAmount === undefined || directAmount === null) {\n return null;\n }\n if (directUnit !== \"usd\" && directUnit !== \"microUsdc\") {\n throw new TypeError(\n `Amount input must be tagged with unit:\"usd\" or unit:\"microUsdc\"; got unit=${JSON.stringify(directUnit)}. GUARD-05 refuses to guess - specify explicitly.`\n );\n }\n if (directUnit === \"microUsdc\") {\n if (\n !(typeof directAmount === \"string\" && MICRO_USDC_RE.test(directAmount))\n ) {\n throw new TypeError(\n `unit:\"microUsdc\" requires amount as a non-negative integer string; got ${typeof directAmount}`\n );\n }\n return BigInt(directAmount);\n }\n // unit === \"usd\"\n if (\n !(\n typeof directAmount === \"number\" &&\n Number.isFinite(directAmount) &&\n directAmount >= 0\n )\n ) {\n throw new TypeError(\n `unit:\"usd\" requires amount as a finite non-negative number; got ${typeof directAmount}`\n );\n }\n return BigInt(Math.round(directAmount * USDC_DECIMALS));\n}\n\nfunction extractContractAddress(input: HookInput): string | null {\n const ti = input.tool_input ?? {};\n const challenge = (ti.paymentChallenge ?? {}) as Record<string, unknown>;\n // Precedence order:\n // 1. challenge.asset — x402 TransferWithAuthorization: the ERC-20 contract\n // the authorization is bound to (the EVM `eth.tx.to` at execution time).\n // This mirrors the server-side Turnkey policy (policy.ts) which denies\n // `eth.tx.to not in [USDC_BASE, USDC_TEMPO]`.\n // 2. ti.contract / ti.assetAddress — agent-runtime-supplied hints.\n // 3. ti.to / challenge.to — legacy tool_inputs that labeled the asset as\n // \"to\" (some older MCP implementations). Kept for backwards compat.\n // NEVER reads challenge.payTo: that is the transfer recipient (the\n // facilitator or service operator), not the ERC-20 contract being invoked.\n const contract =\n challenge.asset ??\n ti.contract ??\n ti.assetAddress ??\n ti.to ??\n challenge.to;\n if (typeof contract === \"string\" && ADDRESS_RE.test(contract)) {\n return contract.toLowerCase();\n }\n return null;\n}\n\nfunction usdToMicro(usd: number): bigint {\n return BigInt(Math.round(usd * USDC_DECIMALS));\n}\n\n/**\n * Factory returning the PreToolUse hook function. The hook enforces the three\n * client-side safety tiers (auto / ask / block) sourced EXCLUSIVELY from\n * ~/.keeperhub/safety.json -- never from the tool payload (GUARD-05).\n */\nexport async function createPreToolUseHook(\n options: CreateHookOptions = {}\n): Promise<(input: unknown) => Promise<HookDecision>> {\n const toolMatcher = options.toolNameMatcher ?? defaultToolMatcher;\n const configLoader = options.configLoader ?? loadSafetyConfig;\n const walletLoader = options.walletLoader ?? readWalletConfig;\n const clientFactory =\n options.clientFactory ?? ((w: WalletConfig) => new KeeperHubClient(w));\n const onAskOpen =\n options.onAskOpen ??\n ((url: string): void => {\n process.stderr.write(\n `\\n[keeperhub-wallet] Approval required. Visit: ${url}\\n`\n );\n });\n const poll = options.poll ?? DEFAULT_POLL;\n\n const safety = await configLoader();\n\n return async (raw: unknown): Promise<HookDecision> => {\n const hookInput = (raw ?? {}) as HookInput;\n\n // Pass-through for non-wallet tool calls.\n if (\n !(\n typeof hookInput.tool_name === \"string\" &&\n toolMatcher(hookInput.tool_name)\n )\n ) {\n return { decision: \"allow\" };\n }\n\n // GUARD-05: ONLY these fields. No trust/override/admin_* reads.\n const contractAddr = extractContractAddress(hookInput);\n const amountMicro = extractAmountMicroUsdc(hookInput);\n\n if (contractAddr && !safety.allowlisted_contracts.includes(contractAddr)) {\n return { decision: \"deny\", reason: \"CONTRACT_NOT_ALLOWLISTED\" };\n }\n\n if (amountMicro === null) {\n return { decision: \"deny\", reason: \"AMOUNT_UNDETERMINED\" };\n }\n\n const blockMicro = usdToMicro(safety.block_threshold_usd);\n const askMicro = usdToMicro(safety.ask_threshold_usd);\n const autoMicro = usdToMicro(safety.auto_approve_max_usd);\n\n if (amountMicro > blockMicro) {\n return { decision: \"deny\", reason: \"BLOCKED_BY_SAFETY_RULE\" };\n }\n\n if (amountMicro >= askMicro) {\n // Open approval flow (create approval-request + poll until non-pending).\n const wallet = await walletLoader();\n const client = clientFactory(wallet);\n // Server returns `{id}` on create; poll endpoint returns `{status}`.\n const created = await client.request<{\n id: string;\n }>(\"POST\", \"/api/agentic-wallet/approval-request\", {\n // Server contract (Phase 33): riskLevel MUST be 'ask' or 'block';\n // operationPayload MUST be a non-array object. The hook only creates\n // approval-requests at the ask tier (block tier short-circuits above).\n riskLevel: \"ask\",\n operationPayload: {\n amountMicroUsdc: amountMicro.toString(),\n contractAddress: contractAddr ?? \"\",\n toolName: hookInput.tool_name ?? \"\",\n reason: `Agent tool ${hookInput.tool_name}`,\n },\n });\n const approvalId =\n \"_status\" in created ? created.approvalRequestId : created.id;\n onAskOpen(`${APPROVAL_URL_BASE}${approvalId}`);\n for (let attempt = 0; attempt < poll.maxAttempts; attempt++) {\n await new Promise<void>((r) => setTimeout(r, poll.intervalMs));\n const status = await client.request<{\n status: \"pending\" | \"approved\" | \"rejected\";\n }>(\"GET\", `/api/agentic-wallet/approval-request/${approvalId}`);\n // WR-03: align with payment-signer.ts:84 -- positive-shape check on\n // the expected envelope. If a future server change or proxy returns\n // a 202/other wrapper without `status`, keep polling instead of\n // treating an unknown envelope as \"pending\" by implication.\n if (!(\"status\" in status)) {\n continue;\n }\n if (status.status === \"approved\") {\n return { decision: \"allow\" };\n }\n if (status.status === \"rejected\") {\n return { decision: \"deny\", reason: \"USER_REJECTED\" };\n }\n // status === \"pending\" -- continue polling.\n }\n return { decision: \"deny\", reason: \"APPROVAL_TIMEOUT\" };\n }\n\n if (amountMicro <= autoMicro) {\n return { decision: \"allow\" };\n }\n\n // Middle band: above auto but below ask.\n return { decision: \"ask\" };\n };\n}\n","import { createPreToolUseHook } from \"./hook.js\";\n\n/**\n * Binary entrypoint for `npx @keeperhub/wallet hook` or direct invocation via\n * Claude Code settings.json:\n *\n * { \"type\": \"command\", \"command\": \"npx @keeperhub/wallet hook\", \"timeout\": 30 }\n *\n * Reads JSON from stdin (Claude Code PreToolUse payload), writes the JSON\n * decision envelope to stdout, and exits 2 on deny (the universal \"block\"\n * signal across agent-hook runtimes per the Claude Code docs). A non-JSON\n * stdin is treated as a deny.\n *\n * @security Stdout is RESERVED for the envelope JSON; any diagnostic output\n * (approval URL, errors) goes to stderr via onAskOpen or direct writes.\n */\nexport async function runHookCli(): Promise<void> {\n const hook = await createPreToolUseHook();\n\n let raw = \"\";\n for await (const chunk of process.stdin as unknown as AsyncIterable<Buffer>) {\n raw += chunk.toString(\"utf-8\");\n }\n\n let parsed: unknown;\n try {\n parsed = raw.trim().length > 0 ? JSON.parse(raw) : {};\n } catch (err) {\n process.stderr.write(\n `[keeperhub-wallet] hook input is not valid JSON: ${(err as Error).message}\\n`\n );\n process.exit(2);\n }\n\n const decision = await hook(parsed);\n\n const output = {\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\" as const,\n permissionDecision: decision.decision,\n ...(decision.reason ? { permissionDecisionReason: decision.reason } : {}),\n },\n };\n process.stdout.write(JSON.stringify(output));\n process.exit(decision.decision === \"deny\" ? 2 : 0);\n}\n"],"mappings":";AAAA,SAAS,YAAY,kBAAkB;AAahC,SAAS,iBACd,QACA,QACA,MACA,UACA,MACA,WACQ;AACR,QAAM,aAAa,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACjE,QAAM,gBAAgB,GAAG,MAAM;AAAA,EAAK,IAAI;AAAA,EAAK,QAAQ;AAAA,EAAK,UAAU;AAAA,EAAK,SAAS;AAClF,SAAO,WAAW,UAAU,MAAM,EAAE,OAAO,aAAa,EAAE,OAAO,KAAK;AACxE;AASO,SAAS,iBACd,QACA,QACA,MACA,UACA,MACa;AACb,QAAM,YAAY,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,CAAC;AACtD,QAAM,YAAY;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,EACpB;AACF;;;ACjCO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EAC/B;AAAA,EAET,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,2BAAN,cAAuC,MAAM;AAAA,EAClD,cAAc;AACZ;AAAA,MACE;AAAA,IACF;AACA,SAAK,OAAO;AAAA,EACd;AACF;;;AClBA,IAAM,iBAAiB;AAEvB,SAAS,qBAAqB,QAAwB;AACpD,MAAI,WAAW,KAAK;AAClB,WAAO;AAAA,EACT;AACA,MAAI,WAAW,KAAK;AAClB,WAAO;AAAA,EACT;AACA,MAAI,WAAW,KAAK;AAClB,WAAO;AAAA,EACT;AACA,MAAI,WAAW,KAAK;AAClB,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,MAAM;AACvB;AAWO,IAAM,kBAAN,MAAsB;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,QAAsB,OAAsB,CAAC,GAAG;AAC1D,SAAK,SAAS;AACd,UAAM,UAAU,QAAQ,IAAI;AAC5B,SAAK,WACH,KAAK,WACL,WACA,6BACA,QAAQ,gBAAgB,EAAE;AAC5B,SAAK,YAAY,KAAK,SAAS,WAAW;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,QACJ,QACA,MACA,MAC8B;AAC9B,UAAM,UAAU,SAAS,SAAY,KAAK,KAAK,UAAU,IAAI;AAC7D,UAAM,cAAc;AAAA,MAClB,KAAK,OAAO;AAAA,MACZ;AAAA,MACA;AAAA,MACA,KAAK,OAAO;AAAA,MACZ;AAAA,IACF;AACA,UAAM,UACJ,WAAW,SACP,EAAE,GAAG,aAAa,gBAAgB,mBAAmB,IACrD,EAAE,GAAG,YAAY;AACvB,UAAM,WAAW,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,MAC9D;AAAA,MACA;AAAA,MACA,MAAM,WAAW,SAAS,UAAU;AAAA,IACtC,CAAC;AAED,QAAI,SAAS,WAAW,KAAK;AAC3B,YAAM,OAAQ,MAAM,SAAS,KAAK;AAIlC,aAAO,EAAE,SAAS,KAAK,mBAAmB,KAAK,kBAAkB;AAAA,IACnE;AAEA,QAAI,CAAC,SAAS,IAAI;AAChB,UAAI,OAAO;AACX,UAAI,UAAU,QAAQ,SAAS,MAAM;AACrC,UAAI;AACF,cAAM,OAAQ,MAAM,SAAS,KAAK;AAIlC,eAAO,KAAK,QAAQ,qBAAqB,SAAS,MAAM;AACxD,kBAAU,KAAK,SAAS;AAAA,MAC1B,QAAQ;AAAA,MAER;AACA,YAAM,IAAI,eAAe,MAAM,OAAO;AAAA,IACxC;AAEA,WAAQ,MAAM,SAAS,KAAK;AAAA,EAC9B;AACF;;;AC3HA,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAoBvB,IAAM,wBAAsC;AAAA,EACjD,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,uBAAuB;AAAA,IACrB;AAAA;AAAA,IACA;AAAA;AAAA,EACF;AACF;AAOA,SAAS,gBAAwB;AAC/B,SAAO,KAAK,QAAQ,GAAG,cAAc,aAAa;AACpD;AAEA,eAAsB,mBAA0C;AAC9D,QAAM,OAAO,cAAc;AAC3B,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS,MAAM,OAAO;AAAA,EACpC,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,YAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAC3D,YAAM,UAAU,MAAM,KAAK,UAAU,uBAAuB,MAAM,CAAC,GAAG;AAAA,QACpE,MAAM;AAAA,MACR,CAAC;AAED,YAAM,MAAM,MAAM,GAAK;AACvB,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACA,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,SAAO,iBAAiB,MAAM;AAChC;AAEA,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,iBAAiB,SAA8C;AAC7E,QAAM,SAAuB;AAAA,IAC3B,sBACE,QAAQ,wBACR,sBAAsB;AAAA,IACxB,mBACE,QAAQ,qBAAqB,sBAAsB;AAAA,IACrD,qBACE,QAAQ,uBAAuB,sBAAsB;AAAA,IACvD,uBACE,QAAQ,yBACR,sBAAsB;AAAA,EAC1B;AAEA,aAAW,OAAO,gBAAgB;AAChC,UAAM,IAAI,OAAO,GAAG;AACpB,QAAI,EAAE,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI;AACnC,YAAM,IAAI;AAAA,QACR,gBAAgB,GAAG,8CAA8C,OAAO,CAAC,CAAC;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACA,MAAI,OAAO,oBAAoB,OAAO,sBAAsB;AAC1D,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,OAAO,sBAAsB,OAAO,mBAAmB;AACzD,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,MAAM,QAAQ,OAAO,qBAAqB,GAAG;AAChD,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO,wBAAwB,OAAO,sBAAsB;AAAA,IAAI,CAAC,MAC/D,EAAE,YAAY;AAAA,EAChB;AACA,SAAO;AACT;;;AC3GA,SAAS,SAAAA,QAAO,SAAAC,QAAO,YAAAC,WAAU,aAAAC,kBAAiB;AAClD,SAAS,WAAAC,gBAAe;AACxB,SAAS,WAAAC,UAAS,QAAAC,aAAY;AAU9B,eAAsB,mBAA0C;AAC9D,QAAM,aAAaC,MAAKC,SAAQ,GAAG,cAAc,aAAa;AAC9D,MAAI;AACJ,MAAI;AACF,UAAM,MAAMC,UAAS,YAAY,OAAO;AAAA,EAC1C,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,YAAM,IAAI,yBAAyB;AAAA,IACrC;AACA,UAAM;AAAA,EACR;AACA,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,MAAI,EAAE,OAAO,YAAY,OAAO,iBAAiB,OAAO,aAAa;AACnE,UAAM,IAAI,MAAM,4BAA4B,UAAU,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;;;ACAA,IAAM,eAAe,EAAE,YAAY,KAAM,aAAa,IAAI;AAC1D,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB;AACtB,IAAM,aAAa;AACnB,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAExB,SAAS,mBAAmB,MAAuB;AACjD,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAkBA,SAAS,uBAAuB,OAAiC;AAC/D,QAAM,KAAK,MAAM,cAAc,CAAC;AAChC,QAAM,YAAa,GAAG,oBAAoB,CAAC;AAQ3C,QAAM,eAAe,UAAU,UAAU,GAAG;AAC5C,QAAM,aAAa,UAAU,QAAQ,GAAG;AAExC,MAAI,iBAAiB,UAAa,iBAAiB,MAAM;AACvD,WAAO;AAAA,EACT;AACA,MAAI,eAAe,SAAS,eAAe,aAAa;AACtD,UAAM,IAAI;AAAA,MACR,6EAA6E,KAAK,UAAU,UAAU,CAAC;AAAA,IACzG;AAAA,EACF;AACA,MAAI,eAAe,aAAa;AAC9B,QACE,EAAE,OAAO,iBAAiB,YAAY,cAAc,KAAK,YAAY,IACrE;AACA,YAAM,IAAI;AAAA,QACR,0EAA0E,OAAO,YAAY;AAAA,MAC/F;AAAA,IACF;AACA,WAAO,OAAO,YAAY;AAAA,EAC5B;AAEA,MACE,EACE,OAAO,iBAAiB,YACxB,OAAO,SAAS,YAAY,KAC5B,gBAAgB,IAElB;AACA,UAAM,IAAI;AAAA,MACR,mEAAmE,OAAO,YAAY;AAAA,IACxF;AAAA,EACF;AACA,SAAO,OAAO,KAAK,MAAM,eAAe,aAAa,CAAC;AACxD;AAEA,SAAS,uBAAuB,OAAiC;AAC/D,QAAM,KAAK,MAAM,cAAc,CAAC;AAChC,QAAM,YAAa,GAAG,oBAAoB,CAAC;AAW3C,QAAM,WACJ,UAAU,SACV,GAAG,YACH,GAAG,gBACH,GAAG,MACH,UAAU;AACZ,MAAI,OAAO,aAAa,YAAY,WAAW,KAAK,QAAQ,GAAG;AAC7D,WAAO,SAAS,YAAY;AAAA,EAC9B;AACA,SAAO;AACT;AAEA,SAAS,WAAW,KAAqB;AACvC,SAAO,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAC/C;AAOA,eAAsB,qBACpB,UAA6B,CAAC,GACsB;AACpD,QAAM,cAAc,QAAQ,mBAAmB;AAC/C,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,gBACJ,QAAQ,kBAAkB,CAAC,MAAoB,IAAI,gBAAgB,CAAC;AACtE,QAAM,YACJ,QAAQ,cACP,CAAC,QAAsB;AACtB,YAAQ,OAAO;AAAA,MACb;AAAA,+CAAkD,GAAG;AAAA;AAAA,IACvD;AAAA,EACF;AACF,QAAM,OAAO,QAAQ,QAAQ;AAE7B,QAAM,SAAS,MAAM,aAAa;AAElC,SAAO,OAAO,QAAwC;AACpD,UAAM,YAAa,OAAO,CAAC;AAG3B,QACE,EACE,OAAO,UAAU,cAAc,YAC/B,YAAY,UAAU,SAAS,IAEjC;AACA,aAAO,EAAE,UAAU,QAAQ;AAAA,IAC7B;AAGA,UAAM,eAAe,uBAAuB,SAAS;AACrD,UAAM,cAAc,uBAAuB,SAAS;AAEpD,QAAI,gBAAgB,CAAC,OAAO,sBAAsB,SAAS,YAAY,GAAG;AACxE,aAAO,EAAE,UAAU,QAAQ,QAAQ,2BAA2B;AAAA,IAChE;AAEA,QAAI,gBAAgB,MAAM;AACxB,aAAO,EAAE,UAAU,QAAQ,QAAQ,sBAAsB;AAAA,IAC3D;AAEA,UAAM,aAAa,WAAW,OAAO,mBAAmB;AACxD,UAAM,WAAW,WAAW,OAAO,iBAAiB;AACpD,UAAM,YAAY,WAAW,OAAO,oBAAoB;AAExD,QAAI,cAAc,YAAY;AAC5B,aAAO,EAAE,UAAU,QAAQ,QAAQ,yBAAyB;AAAA,IAC9D;AAEA,QAAI,eAAe,UAAU;AAE3B,YAAM,SAAS,MAAM,aAAa;AAClC,YAAM,SAAS,cAAc,MAAM;AAEnC,YAAM,UAAU,MAAM,OAAO,QAE1B,QAAQ,wCAAwC;AAAA;AAAA;AAAA;AAAA,QAIjD,WAAW;AAAA,QACX,kBAAkB;AAAA,UAChB,iBAAiB,YAAY,SAAS;AAAA,UACtC,iBAAiB,gBAAgB;AAAA,UACjC,UAAU,UAAU,aAAa;AAAA,UACjC,QAAQ,cAAc,UAAU,SAAS;AAAA,QAC3C;AAAA,MACF,CAAC;AACD,YAAM,aACJ,aAAa,UAAU,QAAQ,oBAAoB,QAAQ;AAC7D,gBAAU,GAAG,iBAAiB,GAAG,UAAU,EAAE;AAC7C,eAAS,UAAU,GAAG,UAAU,KAAK,aAAa,WAAW;AAC3D,cAAM,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,KAAK,UAAU,CAAC;AAC7D,cAAM,SAAS,MAAM,OAAO,QAEzB,OAAO,wCAAwC,UAAU,EAAE;AAK9D,YAAI,EAAE,YAAY,SAAS;AACzB;AAAA,QACF;AACA,YAAI,OAAO,WAAW,YAAY;AAChC,iBAAO,EAAE,UAAU,QAAQ;AAAA,QAC7B;AACA,YAAI,OAAO,WAAW,YAAY;AAChC,iBAAO,EAAE,UAAU,QAAQ,QAAQ,gBAAgB;AAAA,QACrD;AAAA,MAEF;AACA,aAAO,EAAE,UAAU,QAAQ,QAAQ,mBAAmB;AAAA,IACxD;AAEA,QAAI,eAAe,WAAW;AAC5B,aAAO,EAAE,UAAU,QAAQ;AAAA,IAC7B;AAGA,WAAO,EAAE,UAAU,MAAM;AAAA,EAC3B;AACF;;;AC/NA,eAAsB,aAA4B;AAChD,QAAM,OAAO,MAAM,qBAAqB;AAExC,MAAI,MAAM;AACV,mBAAiB,SAAS,QAAQ,OAA2C;AAC3E,WAAO,MAAM,SAAS,OAAO;AAAA,EAC/B;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,KAAK,EAAE,SAAS,IAAI,KAAK,MAAM,GAAG,IAAI,CAAC;AAAA,EACtD,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,oDAAqD,IAAc,OAAO;AAAA;AAAA,IAC5E;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,WAAW,MAAM,KAAK,MAAM;AAElC,QAAM,SAAS;AAAA,IACb,oBAAoB;AAAA,MAClB,eAAe;AAAA,MACf,oBAAoB,SAAS;AAAA,MAC7B,GAAI,SAAS,SAAS,EAAE,0BAA0B,SAAS,OAAO,IAAI,CAAC;AAAA,IACzE;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,KAAK,UAAU,MAAM,CAAC;AAC3C,UAAQ,KAAK,SAAS,aAAa,SAAS,IAAI,CAAC;AACnD;","names":["chmod","mkdir","readFile","writeFile","homedir","dirname","join","join","homedir","readFile"]}
|
|
1
|
+
{"version":3,"sources":["../src/safety-config.ts","../src/hook.ts","../src/hook-entrypoint.ts"],"sourcesContent":["import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * User-owned safety config at ~/.keeperhub/safety.json. File mode 0o644 so the\n * user can freely edit thresholds and the allowlist; server-side Turnkey policy\n * remains the authoritative hard cap (GUARD-06).\n */\nexport type SafetyConfig = {\n auto_approve_max_usd: number;\n ask_threshold_usd: number;\n block_threshold_usd: number;\n allowlisted_contracts: string[];\n};\n\n/**\n * Defaults per 34-CONTEXT lines 61-68. Thresholds bracket the Turnkey policy\n * hard cap (100 USDC). Allowlisted contracts mirror the server Turnkey policy\n * allowlist (lib/agentic-wallet/policy.ts FACILITATOR_ALLOWLIST) -- lowercased\n * for case-insensitive match against tool_input.to / paymentChallenge.payTo.\n */\nexport const DEFAULT_SAFETY_CONFIG: SafetyConfig = {\n auto_approve_max_usd: 5,\n ask_threshold_usd: 50,\n block_threshold_usd: 100,\n allowlisted_contracts: [\n \"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\", // Base USDC\n \"0x20c000000000000000000000b9537d11c60e8b50\", // Tempo USDC.e\n ],\n};\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"safety.json\")`\n// itself -- matches storage.ts. Hoisting to a module-level constant would\n// freeze $HOME at import time and break tests that override process.env.HOME\n// in beforeEach.\n\nfunction getSafetyPath(): string {\n return join(homedir(), \".keeperhub\", \"safety.json\");\n}\n\nexport async function loadSafetyConfig(): Promise<SafetyConfig> {\n const path = getSafetyPath();\n let raw: string;\n try {\n raw = await readFile(path, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n await mkdir(dirname(path), { recursive: true, mode: 0o700 });\n await writeFile(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {\n mode: 0o644,\n });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(path, 0o644);\n return DEFAULT_SAFETY_CONFIG;\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<SafetyConfig>;\n return validateAndMerge(parsed);\n}\n\nconst THRESHOLD_KEYS = [\n \"auto_approve_max_usd\",\n \"ask_threshold_usd\",\n \"block_threshold_usd\",\n] as const;\n\nexport function validateAndMerge(partial: Partial<SafetyConfig>): SafetyConfig {\n const merged: SafetyConfig = {\n auto_approve_max_usd:\n partial.auto_approve_max_usd ??\n DEFAULT_SAFETY_CONFIG.auto_approve_max_usd,\n ask_threshold_usd:\n partial.ask_threshold_usd ?? DEFAULT_SAFETY_CONFIG.ask_threshold_usd,\n block_threshold_usd:\n partial.block_threshold_usd ?? DEFAULT_SAFETY_CONFIG.block_threshold_usd,\n allowlisted_contracts:\n partial.allowlisted_contracts ??\n DEFAULT_SAFETY_CONFIG.allowlisted_contracts,\n };\n\n for (const key of THRESHOLD_KEYS) {\n const v = merged[key];\n if (!(Number.isFinite(v) && v >= 0)) {\n throw new Error(\n `safety.json: ${key} must be a non-negative finite number; got ${String(v)}`\n );\n }\n }\n if (merged.ask_threshold_usd < merged.auto_approve_max_usd) {\n throw new Error(\n \"safety.json: ask_threshold_usd must be >= auto_approve_max_usd\"\n );\n }\n if (merged.block_threshold_usd < merged.ask_threshold_usd) {\n throw new Error(\n \"safety.json: block_threshold_usd must be >= ask_threshold_usd\"\n );\n }\n if (!Array.isArray(merged.allowlisted_contracts)) {\n throw new Error(\"safety.json: allowlisted_contracts must be an array\");\n }\n merged.allowlisted_contracts = merged.allowlisted_contracts.map((a) =>\n a.toLowerCase()\n );\n return merged;\n}\n\nexport function getSafetyConfigPath(): string {\n return getSafetyPath();\n}\n","import { loadSafetyConfig, type SafetyConfig } from \"./safety-config.js\";\nimport type { HookDecision } from \"./types.js\";\n\ntype HookInput = {\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n};\n\nexport type CreateHookOptions = {\n /** Match against tool_name. Default: /keeperhub|wallet|sign/i */\n toolNameMatcher?: (name: string) => boolean;\n /** Injected for tests */\n configLoader?: () => Promise<SafetyConfig>;\n};\n\nconst USDC_DECIMALS = 1_000_000;\nconst ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\nconst MICRO_USDC_RE = /^\\d+$/;\nconst DEFAULT_TOOL_RE = /keeperhub|wallet|sign/i;\n\nfunction defaultToolMatcher(name: string): boolean {\n return DEFAULT_TOOL_RE.test(name);\n}\n\n/**\n * Coerce an amount field to micro-USDC. Inputs MUST be explicitly tagged with\n * `unit`:\n * - `{amount: string, unit: \"microUsdc\"}` -> parsed as integer micro-USDC\n * (x402 wire format)\n * - `{amount: number, unit: \"usd\"}` -> multiplied by 1_000_000\n *\n * Untagged amounts are REJECTED with a thrown TypeError. This is GUARD-05:\n * we refuse to guess whether a \"5\" is 5 USD or 5 micro-USDC (a six-order-of-\n * magnitude reading error). The caller must commit.\n *\n * Fields read: ONLY tool_input.paymentChallenge.{amount,unit} and\n * tool_input.{amount,unit}. Forged safety-bypass fields (any \"trust-level\"\n * hint, \"is-safe\" boolean, \"admin-override\" bit, or similar) are NEVER read;\n * thresholds come exclusively from ~/.keeperhub/safety.json.\n */\nfunction extractAmountMicroUsdc(input: HookInput): bigint | null {\n const ti = input.tool_input ?? {};\n const challenge = (ti.paymentChallenge ?? {}) as Record<string, unknown>;\n // WR-01: prefer the signed wire field (paymentChallenge.amount/unit) over\n // caller-supplied sibling tool_input fields. The nested challenge is the\n // field the downstream /sign call actually binds into the signed bytes, so\n // a misbehaving tool cannot slip a larger nested amount past the auto cap\n // by shadowing it with a small top-level sibling. Fall back to top-level\n // only when no challenge is present (e.g. direct /sign tool calls with no\n // 402 round).\n const directAmount = challenge.amount ?? ti.amount;\n const directUnit = challenge.unit ?? ti.unit;\n\n if (directAmount === undefined || directAmount === null) {\n return null;\n }\n if (directUnit !== \"usd\" && directUnit !== \"microUsdc\") {\n throw new TypeError(\n `Amount input must be tagged with unit:\"usd\" or unit:\"microUsdc\"; got unit=${JSON.stringify(directUnit)}. GUARD-05 refuses to guess - specify explicitly.`\n );\n }\n if (directUnit === \"microUsdc\") {\n if (\n !(typeof directAmount === \"string\" && MICRO_USDC_RE.test(directAmount))\n ) {\n throw new TypeError(\n `unit:\"microUsdc\" requires amount as a non-negative integer string; got ${typeof directAmount}`\n );\n }\n return BigInt(directAmount);\n }\n // unit === \"usd\"\n if (\n !(\n typeof directAmount === \"number\" &&\n Number.isFinite(directAmount) &&\n directAmount >= 0\n )\n ) {\n throw new TypeError(\n `unit:\"usd\" requires amount as a finite non-negative number; got ${typeof directAmount}`\n );\n }\n return BigInt(Math.round(directAmount * USDC_DECIMALS));\n}\n\nfunction extractContractAddress(input: HookInput): string | null {\n const ti = input.tool_input ?? {};\n const challenge = (ti.paymentChallenge ?? {}) as Record<string, unknown>;\n // Precedence order:\n // 1. challenge.asset -- x402 TransferWithAuthorization: the ERC-20 contract\n // the authorization is bound to (the EVM `eth.tx.to` at execution time).\n // This mirrors the server-side Turnkey policy (policy.ts) which denies\n // `eth.tx.to not in [USDC_BASE, USDC_TEMPO]`.\n // 2. ti.contract / ti.assetAddress -- agent-runtime-supplied hints.\n // 3. ti.to / challenge.to -- legacy tool_inputs that labeled the asset as\n // \"to\" (some older MCP implementations). Kept for backwards compat.\n // NEVER reads challenge.payTo: that is the transfer recipient (the\n // facilitator or service operator), not the ERC-20 contract being invoked.\n const contract =\n challenge.asset ??\n ti.contract ??\n ti.assetAddress ??\n ti.to ??\n challenge.to;\n if (typeof contract === \"string\" && ADDRESS_RE.test(contract)) {\n return contract.toLowerCase();\n }\n return null;\n}\n\nfunction usdToMicro(usd: number): bigint {\n return BigInt(Math.round(usd * USDC_DECIMALS));\n}\n\n/**\n * Factory returning the PreToolUse hook function. The hook enforces three\n * client-side safety tiers (auto / ask / block) sourced EXCLUSIVELY from\n * ~/.keeperhub/safety.json -- never from the tool payload (GUARD-05).\n *\n * v0.1.4 collapsed the previous four-band behaviour into three:\n *\n * amount <= auto_approve_max_usd -> {decision: \"allow\"}\n * auto_approve_max_usd < amount <= block_threshold -> {decision: \"ask\"} (Claude Code prompts user inline)\n * amount > block_threshold -> {decision: \"deny\"}\n *\n * The previous server-approval branch (amount >= ask_threshold -> create a\n * /api/agentic-wallet/approval-request row, print an approval URL, poll for\n * browser approval) was removed. It required the wallet to be linked to a\n * KeeperHub user via /link, and the link command was rough enough that we\n * never wired it into the documented flow. Returning {decision: \"ask\"}\n * inline lets Claude Code surface the prompt in the agent chat directly.\n *\n * `ask_threshold_usd` is still read from safety.json for backward-compat\n * with existing configs but is not consulted for decision-making. Tracked\n * as KEEP-307 for the permanent architectural decision.\n */\nexport async function createPreToolUseHook(\n options: CreateHookOptions = {}\n): Promise<(input: unknown) => Promise<HookDecision>> {\n const toolMatcher = options.toolNameMatcher ?? defaultToolMatcher;\n const configLoader = options.configLoader ?? loadSafetyConfig;\n\n const safety = await configLoader();\n\n // The hook function is declared async so that synchronous throws in\n // extractAmountMicroUsdc (GUARD-05 unit-tag enforcement) become rejected\n // promises, matching the original pre-0.1.4 behaviour the tests rely on.\n return async (raw: unknown): Promise<HookDecision> => {\n const hookInput = (raw ?? {}) as HookInput;\n\n // Pass-through for non-wallet tool calls.\n if (\n !(\n typeof hookInput.tool_name === \"string\" &&\n toolMatcher(hookInput.tool_name)\n )\n ) {\n return { decision: \"allow\" };\n }\n\n // GUARD-05: ONLY these fields. No trust/override/admin_* reads.\n const contractAddr = extractContractAddress(hookInput);\n const amountMicro = extractAmountMicroUsdc(hookInput);\n\n if (contractAddr && !safety.allowlisted_contracts.includes(contractAddr)) {\n return { decision: \"deny\", reason: \"CONTRACT_NOT_ALLOWLISTED\" };\n }\n\n if (amountMicro === null) {\n return { decision: \"deny\", reason: \"AMOUNT_UNDETERMINED\" };\n }\n\n const blockMicro = usdToMicro(safety.block_threshold_usd);\n const autoMicro = usdToMicro(safety.auto_approve_max_usd);\n\n if (amountMicro > blockMicro) {\n return { decision: \"deny\", reason: \"BLOCKED_BY_SAFETY_RULE\" };\n }\n\n if (amountMicro <= autoMicro) {\n return { decision: \"allow\" };\n }\n\n // Everything between auto and block is an inline ask -- Claude Code\n // surfaces the prompt in-chat.\n return { decision: \"ask\" };\n };\n}\n","import { createPreToolUseHook } from \"./hook.js\";\n\n/**\n * Binary entrypoint for `npx @keeperhub/wallet hook` or direct invocation via\n * Claude Code settings.json:\n *\n * { \"type\": \"command\", \"command\": \"npx @keeperhub/wallet hook\", \"timeout\": 30 }\n *\n * Reads JSON from stdin (Claude Code PreToolUse payload), writes the JSON\n * decision envelope to stdout, and exits 2 on deny (the universal \"block\"\n * signal across agent-hook runtimes per the Claude Code docs). A non-JSON\n * stdin is treated as a deny.\n *\n * @security Stdout is RESERVED for the envelope JSON; any diagnostic output\n * (approval URL, errors) goes to stderr via onAskOpen or direct writes.\n */\nexport async function runHookCli(): Promise<void> {\n const hook = await createPreToolUseHook();\n\n let raw = \"\";\n for await (const chunk of process.stdin as unknown as AsyncIterable<Buffer>) {\n raw += chunk.toString(\"utf-8\");\n }\n\n let parsed: unknown;\n try {\n parsed = raw.trim().length > 0 ? JSON.parse(raw) : {};\n } catch (err) {\n process.stderr.write(\n `[keeperhub-wallet] hook input is not valid JSON: ${(err as Error).message}\\n`\n );\n process.exit(2);\n }\n\n const decision = await hook(parsed);\n\n const output = {\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\" as const,\n permissionDecision: decision.decision,\n ...(decision.reason ? { permissionDecisionReason: decision.reason } : {}),\n },\n };\n process.stdout.write(JSON.stringify(output));\n process.exit(decision.decision === \"deny\" ? 2 : 0);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -194,34 +194,6 @@ function fund(walletAddress) {
|
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// src/hmac.ts
|
|
198
|
-
var import_node_crypto = require("crypto");
|
|
199
|
-
function computeSignature(secret, method, path, subOrgId, body, timestamp) {
|
|
200
|
-
const bodyDigest = (0, import_node_crypto.createHash)("sha256").update(body).digest("hex");
|
|
201
|
-
const signingString = `${method}
|
|
202
|
-
${path}
|
|
203
|
-
${subOrgId}
|
|
204
|
-
${bodyDigest}
|
|
205
|
-
${timestamp}`;
|
|
206
|
-
return (0, import_node_crypto.createHmac)("sha256", secret).update(signingString).digest("hex");
|
|
207
|
-
}
|
|
208
|
-
function buildHmacHeaders(secret, method, path, subOrgId, body) {
|
|
209
|
-
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
210
|
-
const signature = computeSignature(
|
|
211
|
-
secret,
|
|
212
|
-
method,
|
|
213
|
-
path,
|
|
214
|
-
subOrgId,
|
|
215
|
-
body,
|
|
216
|
-
timestamp
|
|
217
|
-
);
|
|
218
|
-
return {
|
|
219
|
-
"X-KH-Sub-Org": subOrgId,
|
|
220
|
-
"X-KH-Timestamp": timestamp,
|
|
221
|
-
"X-KH-Signature": signature
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
197
|
// src/skill-install.ts
|
|
226
198
|
var import_promises = require("fs/promises");
|
|
227
199
|
var import_node_path2 = require("path");
|
|
@@ -433,47 +405,6 @@ async function cmdAdd(opts = {}) {
|
|
|
433
405
|
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
434
406
|
`);
|
|
435
407
|
}
|
|
436
|
-
async function cmdLink(opts = {}) {
|
|
437
|
-
const wallet = await readWalletConfig();
|
|
438
|
-
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
439
|
-
const sessionCookie = process.env.KH_SESSION_COOKIE;
|
|
440
|
-
if (!sessionCookie) {
|
|
441
|
-
process.stderr.write(
|
|
442
|
-
"[keeperhub-wallet] link requires KH_SESSION_COOKIE env var.\nSign in at app.keeperhub.com, copy the session cookie, and re-run with:\n KH_SESSION_COOKIE='<cookie>' npx @keeperhub/wallet link\n"
|
|
443
|
-
);
|
|
444
|
-
process.exit(1);
|
|
445
|
-
}
|
|
446
|
-
const body = JSON.stringify({ subOrgId: wallet.subOrgId });
|
|
447
|
-
const headers = buildHmacHeaders(
|
|
448
|
-
wallet.hmacSecret,
|
|
449
|
-
"POST",
|
|
450
|
-
"/api/agentic-wallet/link",
|
|
451
|
-
wallet.subOrgId,
|
|
452
|
-
body
|
|
453
|
-
);
|
|
454
|
-
const response = await fetch(`${baseUrl}/api/agentic-wallet/link`, {
|
|
455
|
-
method: "POST",
|
|
456
|
-
headers: {
|
|
457
|
-
...headers,
|
|
458
|
-
"content-type": "application/json",
|
|
459
|
-
cookie: sessionCookie
|
|
460
|
-
},
|
|
461
|
-
body
|
|
462
|
-
});
|
|
463
|
-
const json = await response.json().catch(() => ({}));
|
|
464
|
-
if (!response.ok) {
|
|
465
|
-
process.stderr.write(
|
|
466
|
-
`[keeperhub-wallet] link failed: ${json.code ?? response.status}: ${json.error ?? ""}
|
|
467
|
-
`
|
|
468
|
-
);
|
|
469
|
-
process.exit(1);
|
|
470
|
-
}
|
|
471
|
-
if (json.already) {
|
|
472
|
-
process.stdout.write("already linked\n");
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
process.stdout.write("linked\n");
|
|
476
|
-
}
|
|
477
408
|
async function cmdFund() {
|
|
478
409
|
const wallet = await readWalletConfig();
|
|
479
410
|
const out = fund(wallet.walletAddress);
|
|
@@ -507,11 +438,6 @@ async function runCli(argv = process.argv) {
|
|
|
507
438
|
program.command("add").description("Provision a new agentic wallet (no account required)").option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
508
439
|
await cmdAdd(opts);
|
|
509
440
|
});
|
|
510
|
-
program.command("link").description(
|
|
511
|
-
"Link the current wallet to your KeeperHub account (requires KH_SESSION_COOKIE env)"
|
|
512
|
-
).option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
513
|
-
await cmdLink(opts);
|
|
514
|
-
});
|
|
515
441
|
program.command("fund").description(
|
|
516
442
|
"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address"
|
|
517
443
|
).action(async () => {
|
|
@@ -572,6 +498,34 @@ async function runCli(argv = process.argv) {
|
|
|
572
498
|
}
|
|
573
499
|
}
|
|
574
500
|
|
|
501
|
+
// src/hmac.ts
|
|
502
|
+
var import_node_crypto = require("crypto");
|
|
503
|
+
function computeSignature(secret, method, path, subOrgId, body, timestamp) {
|
|
504
|
+
const bodyDigest = (0, import_node_crypto.createHash)("sha256").update(body).digest("hex");
|
|
505
|
+
const signingString = `${method}
|
|
506
|
+
${path}
|
|
507
|
+
${subOrgId}
|
|
508
|
+
${bodyDigest}
|
|
509
|
+
${timestamp}`;
|
|
510
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(signingString).digest("hex");
|
|
511
|
+
}
|
|
512
|
+
function buildHmacHeaders(secret, method, path, subOrgId, body) {
|
|
513
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
514
|
+
const signature = computeSignature(
|
|
515
|
+
secret,
|
|
516
|
+
method,
|
|
517
|
+
path,
|
|
518
|
+
subOrgId,
|
|
519
|
+
body,
|
|
520
|
+
timestamp
|
|
521
|
+
);
|
|
522
|
+
return {
|
|
523
|
+
"X-KH-Sub-Org": subOrgId,
|
|
524
|
+
"X-KH-Timestamp": timestamp,
|
|
525
|
+
"X-KH-Signature": signature
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
575
529
|
// src/client.ts
|
|
576
530
|
var TRAILING_SLASH2 = /\/$/;
|
|
577
531
|
function defaultCodeForStatus(status) {
|
|
@@ -724,8 +678,6 @@ function getSafetyConfigPath() {
|
|
|
724
678
|
}
|
|
725
679
|
|
|
726
680
|
// src/hook.ts
|
|
727
|
-
var DEFAULT_POLL = { intervalMs: 2e3, maxAttempts: 150 };
|
|
728
|
-
var APPROVAL_URL_BASE = "https://app.keeperhub.com/approve/";
|
|
729
681
|
var USDC_DECIMALS2 = 1e6;
|
|
730
682
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
731
683
|
var MICRO_USDC_RE = /^\d+$/;
|
|
@@ -776,16 +728,6 @@ function usdToMicro(usd) {
|
|
|
776
728
|
async function createPreToolUseHook(options = {}) {
|
|
777
729
|
const toolMatcher = options.toolNameMatcher ?? defaultToolMatcher;
|
|
778
730
|
const configLoader = options.configLoader ?? loadSafetyConfig;
|
|
779
|
-
const walletLoader = options.walletLoader ?? readWalletConfig;
|
|
780
|
-
const clientFactory = options.clientFactory ?? ((w) => new KeeperHubClient(w));
|
|
781
|
-
const onAskOpen = options.onAskOpen ?? ((url) => {
|
|
782
|
-
process.stderr.write(
|
|
783
|
-
`
|
|
784
|
-
[keeperhub-wallet] Approval required. Visit: ${url}
|
|
785
|
-
`
|
|
786
|
-
);
|
|
787
|
-
});
|
|
788
|
-
const poll = options.poll ?? DEFAULT_POLL;
|
|
789
731
|
const safety = await configLoader();
|
|
790
732
|
return async (raw) => {
|
|
791
733
|
const hookInput = raw ?? {};
|
|
@@ -801,43 +743,10 @@ async function createPreToolUseHook(options = {}) {
|
|
|
801
743
|
return { decision: "deny", reason: "AMOUNT_UNDETERMINED" };
|
|
802
744
|
}
|
|
803
745
|
const blockMicro = usdToMicro(safety.block_threshold_usd);
|
|
804
|
-
const askMicro = usdToMicro(safety.ask_threshold_usd);
|
|
805
746
|
const autoMicro = usdToMicro(safety.auto_approve_max_usd);
|
|
806
747
|
if (amountMicro > blockMicro) {
|
|
807
748
|
return { decision: "deny", reason: "BLOCKED_BY_SAFETY_RULE" };
|
|
808
749
|
}
|
|
809
|
-
if (amountMicro >= askMicro) {
|
|
810
|
-
const wallet = await walletLoader();
|
|
811
|
-
const client = clientFactory(wallet);
|
|
812
|
-
const created = await client.request("POST", "/api/agentic-wallet/approval-request", {
|
|
813
|
-
// Server contract (Phase 33): riskLevel MUST be 'ask' or 'block';
|
|
814
|
-
// operationPayload MUST be a non-array object. The hook only creates
|
|
815
|
-
// approval-requests at the ask tier (block tier short-circuits above).
|
|
816
|
-
riskLevel: "ask",
|
|
817
|
-
operationPayload: {
|
|
818
|
-
amountMicroUsdc: amountMicro.toString(),
|
|
819
|
-
contractAddress: contractAddr ?? "",
|
|
820
|
-
toolName: hookInput.tool_name ?? "",
|
|
821
|
-
reason: `Agent tool ${hookInput.tool_name}`
|
|
822
|
-
}
|
|
823
|
-
});
|
|
824
|
-
const approvalId = "_status" in created ? created.approvalRequestId : created.id;
|
|
825
|
-
onAskOpen(`${APPROVAL_URL_BASE}${approvalId}`);
|
|
826
|
-
for (let attempt = 0; attempt < poll.maxAttempts; attempt++) {
|
|
827
|
-
await new Promise((r) => setTimeout(r, poll.intervalMs));
|
|
828
|
-
const status = await client.request("GET", `/api/agentic-wallet/approval-request/${approvalId}`);
|
|
829
|
-
if (!("status" in status)) {
|
|
830
|
-
continue;
|
|
831
|
-
}
|
|
832
|
-
if (status.status === "approved") {
|
|
833
|
-
return { decision: "allow" };
|
|
834
|
-
}
|
|
835
|
-
if (status.status === "rejected") {
|
|
836
|
-
return { decision: "deny", reason: "USER_REJECTED" };
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return { decision: "deny", reason: "APPROVAL_TIMEOUT" };
|
|
840
|
-
}
|
|
841
750
|
if (amountMicro <= autoMicro) {
|
|
842
751
|
return { decision: "allow" };
|
|
843
752
|
}
|
|
@@ -894,6 +803,19 @@ function parseMppChallenge(response) {
|
|
|
894
803
|
// src/payment-signer.ts
|
|
895
804
|
var import_node_crypto2 = require("crypto");
|
|
896
805
|
|
|
806
|
+
// src/workflow-slug.ts
|
|
807
|
+
var KEEPERHUB_WORKFLOW_RE = /\/api\/mcp\/workflows\/([a-zA-Z0-9_-]+)\/call(?:\/?)(?:\?|$|#)/;
|
|
808
|
+
function extractKeeperHubWorkflowSlug(url) {
|
|
809
|
+
if (!url || url.length === 0) {
|
|
810
|
+
return { ok: false, reason: "EMPTY_URL" };
|
|
811
|
+
}
|
|
812
|
+
const match = KEEPERHUB_WORKFLOW_RE.exec(url);
|
|
813
|
+
if (!match || !match[1]) {
|
|
814
|
+
return { ok: false, reason: "URL_PATTERN_MISMATCH" };
|
|
815
|
+
}
|
|
816
|
+
return { ok: true, slug: match[1] };
|
|
817
|
+
}
|
|
818
|
+
|
|
897
819
|
// src/x402-detect.ts
|
|
898
820
|
function isX402Shape(value) {
|
|
899
821
|
if (typeof value !== "object" || value === null) {
|
|
@@ -992,9 +914,17 @@ function createPaymentSigner(opts = {}) {
|
|
|
992
914
|
return result.signature;
|
|
993
915
|
}
|
|
994
916
|
async function payViaMpp(response, mpp, wallet) {
|
|
917
|
+
const slug = extractKeeperHubWorkflowSlug(response.url);
|
|
918
|
+
if (!slug.ok) {
|
|
919
|
+
throw new KeeperHubError(
|
|
920
|
+
"UNSUPPORTED_RECIPIENT",
|
|
921
|
+
`This wallet only signs payments for KeeperHub workflows. The 402 came from a URL that does not match /api/mcp/workflows/<slug>/call (reason: ${slug.reason}). See KEEP-311 for generic x402 support.`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
995
924
|
const client = clientFactory(wallet);
|
|
996
925
|
const signature = await signOrPoll(client, {
|
|
997
926
|
chain: "tempo",
|
|
927
|
+
workflowSlug: slug.slug,
|
|
998
928
|
paymentChallenge: {
|
|
999
929
|
kind: "mpp",
|
|
1000
930
|
serialized: mpp.serialized,
|
|
@@ -1014,6 +944,13 @@ function createPaymentSigner(opts = {}) {
|
|
|
1014
944
|
"x402 challenge has no accepts entries"
|
|
1015
945
|
);
|
|
1016
946
|
}
|
|
947
|
+
const slug = extractKeeperHubWorkflowSlug(x402.resource.url || response.url);
|
|
948
|
+
if (!slug.ok) {
|
|
949
|
+
throw new KeeperHubError(
|
|
950
|
+
"UNSUPPORTED_RECIPIENT",
|
|
951
|
+
`This wallet only signs payments for KeeperHub workflows. The 402 came from a URL that does not match /api/mcp/workflows/<slug>/call (reason: ${slug.reason}). See KEEP-311 for generic x402 support.`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
1017
954
|
const now = Math.floor(Date.now() / 1e3);
|
|
1018
955
|
const validAfter = now - VALID_AFTER_PAST_SLACK_SECONDS;
|
|
1019
956
|
const validBefore = now + accept.maxTimeoutSeconds;
|
|
@@ -1021,6 +958,7 @@ function createPaymentSigner(opts = {}) {
|
|
|
1021
958
|
const client = clientFactory(wallet);
|
|
1022
959
|
const signature = await signOrPoll(client, {
|
|
1023
960
|
chain: "base",
|
|
961
|
+
workflowSlug: slug.slug,
|
|
1024
962
|
paymentChallenge: {
|
|
1025
963
|
kind: "x402",
|
|
1026
964
|
payTo: accept.payTo,
|