@keeperhub/wallet 0.1.2 → 0.1.4
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 +20 -187
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +20 -187
- 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 +128 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -67
- package/dist/index.d.ts +63 -67
- package/dist/index.js +128 -232
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/keeperhub-wallet.skill.md +26 -8
|
@@ -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":"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","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
|
@@ -127,124 +127,6 @@ var tempo = (0, import_viem.defineChain)({
|
|
|
127
127
|
var BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
128
128
|
var TEMPO_USDC_E = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
129
129
|
|
|
130
|
-
// src/hmac.ts
|
|
131
|
-
var import_node_crypto = require("crypto");
|
|
132
|
-
function computeSignature(secret, method, path, subOrgId, body, timestamp) {
|
|
133
|
-
const bodyDigest = (0, import_node_crypto.createHash)("sha256").update(body).digest("hex");
|
|
134
|
-
const signingString = `${method}
|
|
135
|
-
${path}
|
|
136
|
-
${subOrgId}
|
|
137
|
-
${bodyDigest}
|
|
138
|
-
${timestamp}`;
|
|
139
|
-
return (0, import_node_crypto.createHmac)("sha256", secret).update(signingString).digest("hex");
|
|
140
|
-
}
|
|
141
|
-
function buildHmacHeaders(secret, method, path, subOrgId, body) {
|
|
142
|
-
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
143
|
-
const signature = computeSignature(
|
|
144
|
-
secret,
|
|
145
|
-
method,
|
|
146
|
-
path,
|
|
147
|
-
subOrgId,
|
|
148
|
-
body,
|
|
149
|
-
timestamp
|
|
150
|
-
);
|
|
151
|
-
return {
|
|
152
|
-
"X-KH-Sub-Org": subOrgId,
|
|
153
|
-
"X-KH-Timestamp": timestamp,
|
|
154
|
-
"X-KH-Signature": signature
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// src/types.ts
|
|
159
|
-
var KeeperHubError = class extends Error {
|
|
160
|
-
code;
|
|
161
|
-
constructor(code, message) {
|
|
162
|
-
super(message);
|
|
163
|
-
this.name = "KeeperHubError";
|
|
164
|
-
this.code = code;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
var WalletConfigMissingError = class extends Error {
|
|
168
|
-
constructor() {
|
|
169
|
-
super(
|
|
170
|
-
"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
|
|
171
|
-
);
|
|
172
|
-
this.name = "WalletConfigMissingError";
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// src/client.ts
|
|
177
|
-
var TRAILING_SLASH = /\/$/;
|
|
178
|
-
function defaultCodeForStatus(status) {
|
|
179
|
-
if (status === 401) {
|
|
180
|
-
return "HMAC_INVALID";
|
|
181
|
-
}
|
|
182
|
-
if (status === 403) {
|
|
183
|
-
return "POLICY_BLOCKED";
|
|
184
|
-
}
|
|
185
|
-
if (status === 404) {
|
|
186
|
-
return "NOT_FOUND";
|
|
187
|
-
}
|
|
188
|
-
if (status === 502) {
|
|
189
|
-
return "TURNKEY_UPSTREAM";
|
|
190
|
-
}
|
|
191
|
-
return `HTTP_${status}`;
|
|
192
|
-
}
|
|
193
|
-
var KeeperHubClient = class {
|
|
194
|
-
baseUrl;
|
|
195
|
-
fetchImpl;
|
|
196
|
-
wallet;
|
|
197
|
-
constructor(wallet, opts = {}) {
|
|
198
|
-
this.wallet = wallet;
|
|
199
|
-
const envBase = process.env.KEEPERHUB_API_URL;
|
|
200
|
-
this.baseUrl = (opts.baseUrl ?? envBase ?? "https://app.keeperhub.com").replace(TRAILING_SLASH, "");
|
|
201
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* HMAC-signed POST/GET to any /api/agentic-wallet/* route except
|
|
205
|
-
* /provision. Path MUST start with a leading slash. Body is
|
|
206
|
-
* JSON.stringify'd (or the empty string for GET).
|
|
207
|
-
*
|
|
208
|
-
* Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
|
|
209
|
-
* message)` where `code` is the server-supplied field or the default
|
|
210
|
-
* taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
|
|
211
|
-
* `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
|
|
212
|
-
* AskTierResponse envelope.
|
|
213
|
-
*/
|
|
214
|
-
async request(method, path, body) {
|
|
215
|
-
const bodyStr = body === void 0 ? "" : JSON.stringify(body);
|
|
216
|
-
const hmacHeaders = buildHmacHeaders(
|
|
217
|
-
this.wallet.hmacSecret,
|
|
218
|
-
method,
|
|
219
|
-
path,
|
|
220
|
-
this.wallet.subOrgId,
|
|
221
|
-
bodyStr
|
|
222
|
-
);
|
|
223
|
-
const headers = method === "POST" ? { ...hmacHeaders, "content-type": "application/json" } : { ...hmacHeaders };
|
|
224
|
-
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
225
|
-
method,
|
|
226
|
-
headers,
|
|
227
|
-
body: method === "POST" ? bodyStr : void 0
|
|
228
|
-
});
|
|
229
|
-
if (response.status === 202) {
|
|
230
|
-
const data = await response.json();
|
|
231
|
-
return { _status: 202, approvalRequestId: data.approvalRequestId };
|
|
232
|
-
}
|
|
233
|
-
if (!response.ok) {
|
|
234
|
-
let code = "UNKNOWN";
|
|
235
|
-
let message = `HTTP ${response.status}`;
|
|
236
|
-
try {
|
|
237
|
-
const data = await response.json();
|
|
238
|
-
code = data.code ?? defaultCodeForStatus(response.status);
|
|
239
|
-
message = data.error ?? message;
|
|
240
|
-
} catch {
|
|
241
|
-
}
|
|
242
|
-
throw new KeeperHubError(code, message);
|
|
243
|
-
}
|
|
244
|
-
return await response.json();
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
|
|
248
130
|
// src/balance.ts
|
|
249
131
|
var USDC_DECIMALS = 6;
|
|
250
132
|
async function checkBalance(wallet, opts = {}) {
|
|
@@ -256,8 +138,7 @@ async function checkBalance(wallet, opts = {}) {
|
|
|
256
138
|
chain: tempo,
|
|
257
139
|
transport: (0, import_viem2.http)()
|
|
258
140
|
});
|
|
259
|
-
const
|
|
260
|
-
const [baseRaw, tempoRaw, credit] = await Promise.all([
|
|
141
|
+
const [baseRaw, tempoRaw] = await Promise.all([
|
|
261
142
|
baseClient.readContract({
|
|
262
143
|
address: BASE_USDC,
|
|
263
144
|
abi: import_viem2.erc20Abi,
|
|
@@ -269,12 +150,8 @@ async function checkBalance(wallet, opts = {}) {
|
|
|
269
150
|
abi: import_viem2.erc20Abi,
|
|
270
151
|
functionName: "balanceOf",
|
|
271
152
|
args: [wallet.walletAddress]
|
|
272
|
-
})
|
|
273
|
-
khClient.request("GET", "/api/agentic-wallet/credit")
|
|
153
|
+
})
|
|
274
154
|
]);
|
|
275
|
-
if ("_status" in credit) {
|
|
276
|
-
throw new Error("Unexpected 202 response from /api/agentic-wallet/credit");
|
|
277
|
-
}
|
|
278
155
|
return {
|
|
279
156
|
base: {
|
|
280
157
|
chain: "base",
|
|
@@ -287,10 +164,6 @@ async function checkBalance(wallet, opts = {}) {
|
|
|
287
164
|
token: "USDC.e",
|
|
288
165
|
amount: (0, import_viem2.formatUnits)(tempoRaw, USDC_DECIMALS),
|
|
289
166
|
address: wallet.walletAddress
|
|
290
|
-
},
|
|
291
|
-
offChainCredit: {
|
|
292
|
-
amount: credit.amount,
|
|
293
|
-
currency: "USD"
|
|
294
167
|
}
|
|
295
168
|
};
|
|
296
169
|
}
|
|
@@ -420,6 +293,26 @@ async function installSkill(options = {}) {
|
|
|
420
293
|
var import_promises2 = require("fs/promises");
|
|
421
294
|
var import_node_os2 = require("os");
|
|
422
295
|
var import_node_path3 = require("path");
|
|
296
|
+
|
|
297
|
+
// src/types.ts
|
|
298
|
+
var KeeperHubError = class extends Error {
|
|
299
|
+
code;
|
|
300
|
+
constructor(code, message) {
|
|
301
|
+
super(message);
|
|
302
|
+
this.name = "KeeperHubError";
|
|
303
|
+
this.code = code;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
var WalletConfigMissingError = class extends Error {
|
|
307
|
+
constructor() {
|
|
308
|
+
super(
|
|
309
|
+
"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
|
|
310
|
+
);
|
|
311
|
+
this.name = "WalletConfigMissingError";
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/storage.ts
|
|
423
316
|
async function readWalletConfig() {
|
|
424
317
|
const walletPath = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".keeperhub", "wallet.json");
|
|
425
318
|
let raw;
|
|
@@ -448,11 +341,11 @@ function getWalletConfigPath() {
|
|
|
448
341
|
}
|
|
449
342
|
|
|
450
343
|
// src/cli.ts
|
|
451
|
-
var
|
|
344
|
+
var TRAILING_SLASH = /\/$/;
|
|
452
345
|
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
453
346
|
function resolveBaseUrl(override) {
|
|
454
347
|
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
455
|
-
return candidate.replace(
|
|
348
|
+
return candidate.replace(TRAILING_SLASH, "");
|
|
456
349
|
}
|
|
457
350
|
function isNonEmptyString(value) {
|
|
458
351
|
return typeof value === "string" && value.length > 0;
|
|
@@ -512,47 +405,6 @@ async function cmdAdd(opts = {}) {
|
|
|
512
405
|
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
513
406
|
`);
|
|
514
407
|
}
|
|
515
|
-
async function cmdLink(opts = {}) {
|
|
516
|
-
const wallet = await readWalletConfig();
|
|
517
|
-
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
518
|
-
const sessionCookie = process.env.KH_SESSION_COOKIE;
|
|
519
|
-
if (!sessionCookie) {
|
|
520
|
-
process.stderr.write(
|
|
521
|
-
"[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"
|
|
522
|
-
);
|
|
523
|
-
process.exit(1);
|
|
524
|
-
}
|
|
525
|
-
const body = JSON.stringify({ subOrgId: wallet.subOrgId });
|
|
526
|
-
const headers = buildHmacHeaders(
|
|
527
|
-
wallet.hmacSecret,
|
|
528
|
-
"POST",
|
|
529
|
-
"/api/agentic-wallet/link",
|
|
530
|
-
wallet.subOrgId,
|
|
531
|
-
body
|
|
532
|
-
);
|
|
533
|
-
const response = await fetch(`${baseUrl}/api/agentic-wallet/link`, {
|
|
534
|
-
method: "POST",
|
|
535
|
-
headers: {
|
|
536
|
-
...headers,
|
|
537
|
-
"content-type": "application/json",
|
|
538
|
-
cookie: sessionCookie
|
|
539
|
-
},
|
|
540
|
-
body
|
|
541
|
-
});
|
|
542
|
-
const json = await response.json().catch(() => ({}));
|
|
543
|
-
if (!response.ok) {
|
|
544
|
-
process.stderr.write(
|
|
545
|
-
`[keeperhub-wallet] link failed: ${json.code ?? response.status}: ${json.error ?? ""}
|
|
546
|
-
`
|
|
547
|
-
);
|
|
548
|
-
process.exit(1);
|
|
549
|
-
}
|
|
550
|
-
if (json.already) {
|
|
551
|
-
process.stdout.write("already linked\n");
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
process.stdout.write("linked\n");
|
|
555
|
-
}
|
|
556
408
|
async function cmdFund() {
|
|
557
409
|
const wallet = await readWalletConfig();
|
|
558
410
|
const out = fund(wallet.walletAddress);
|
|
@@ -566,14 +418,10 @@ async function cmdFund() {
|
|
|
566
418
|
async function cmdBalance() {
|
|
567
419
|
const wallet = await readWalletConfig();
|
|
568
420
|
const snap = await checkBalance(wallet);
|
|
569
|
-
process.stdout.write(`Base USDC:
|
|
421
|
+
process.stdout.write(`Base USDC: ${snap.base.amount}
|
|
570
422
|
`);
|
|
571
|
-
process.stdout.write(`Tempo USDC.e:
|
|
423
|
+
process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}
|
|
572
424
|
`);
|
|
573
|
-
process.stdout.write(
|
|
574
|
-
`KeeperHub credit: ${snap.offChainCredit.amount} ${snap.offChainCredit.currency}
|
|
575
|
-
`
|
|
576
|
-
);
|
|
577
425
|
}
|
|
578
426
|
async function cmdInfo() {
|
|
579
427
|
const wallet = await readWalletConfig();
|
|
@@ -586,23 +434,16 @@ async function runCli(argv = process.argv) {
|
|
|
586
434
|
const program = new import_commander.Command();
|
|
587
435
|
program.name("keeperhub-wallet").description(
|
|
588
436
|
"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)"
|
|
589
|
-
).version("0.1.
|
|
437
|
+
).version("0.1.3");
|
|
590
438
|
program.command("add").description("Provision a new agentic wallet (no account required)").option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
591
439
|
await cmdAdd(opts);
|
|
592
440
|
});
|
|
593
|
-
program.command("link").description(
|
|
594
|
-
"Link the current wallet to your KeeperHub account (requires KH_SESSION_COOKIE env)"
|
|
595
|
-
).option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
596
|
-
await cmdLink(opts);
|
|
597
|
-
});
|
|
598
441
|
program.command("fund").description(
|
|
599
442
|
"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address"
|
|
600
443
|
).action(async () => {
|
|
601
444
|
await cmdFund();
|
|
602
445
|
});
|
|
603
|
-
program.command("balance").description(
|
|
604
|
-
"Print unified balance: Base USDC + Tempo USDC.e + off-chain KeeperHub credit"
|
|
605
|
-
).action(async () => {
|
|
446
|
+
program.command("balance").description("Print on-chain balance: Base USDC + Tempo USDC.e").action(async () => {
|
|
606
447
|
await cmdBalance();
|
|
607
448
|
});
|
|
608
449
|
program.command("info").description("Print subOrgId and walletAddress from local config").action(async () => {
|
|
@@ -657,6 +498,106 @@ async function runCli(argv = process.argv) {
|
|
|
657
498
|
}
|
|
658
499
|
}
|
|
659
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
|
+
|
|
529
|
+
// src/client.ts
|
|
530
|
+
var TRAILING_SLASH2 = /\/$/;
|
|
531
|
+
function defaultCodeForStatus(status) {
|
|
532
|
+
if (status === 401) {
|
|
533
|
+
return "HMAC_INVALID";
|
|
534
|
+
}
|
|
535
|
+
if (status === 403) {
|
|
536
|
+
return "POLICY_BLOCKED";
|
|
537
|
+
}
|
|
538
|
+
if (status === 404) {
|
|
539
|
+
return "NOT_FOUND";
|
|
540
|
+
}
|
|
541
|
+
if (status === 502) {
|
|
542
|
+
return "TURNKEY_UPSTREAM";
|
|
543
|
+
}
|
|
544
|
+
return `HTTP_${status}`;
|
|
545
|
+
}
|
|
546
|
+
var KeeperHubClient = class {
|
|
547
|
+
baseUrl;
|
|
548
|
+
fetchImpl;
|
|
549
|
+
wallet;
|
|
550
|
+
constructor(wallet, opts = {}) {
|
|
551
|
+
this.wallet = wallet;
|
|
552
|
+
const envBase = process.env.KEEPERHUB_API_URL;
|
|
553
|
+
this.baseUrl = (opts.baseUrl ?? envBase ?? "https://app.keeperhub.com").replace(TRAILING_SLASH2, "");
|
|
554
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* HMAC-signed POST/GET to any /api/agentic-wallet/* route except
|
|
558
|
+
* /provision. Path MUST start with a leading slash. Body is
|
|
559
|
+
* JSON.stringify'd (or the empty string for GET).
|
|
560
|
+
*
|
|
561
|
+
* Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
|
|
562
|
+
* message)` where `code` is the server-supplied field or the default
|
|
563
|
+
* taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
|
|
564
|
+
* `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
|
|
565
|
+
* AskTierResponse envelope.
|
|
566
|
+
*/
|
|
567
|
+
async request(method, path, body) {
|
|
568
|
+
const bodyStr = body === void 0 ? "" : JSON.stringify(body);
|
|
569
|
+
const hmacHeaders = buildHmacHeaders(
|
|
570
|
+
this.wallet.hmacSecret,
|
|
571
|
+
method,
|
|
572
|
+
path,
|
|
573
|
+
this.wallet.subOrgId,
|
|
574
|
+
bodyStr
|
|
575
|
+
);
|
|
576
|
+
const headers = method === "POST" ? { ...hmacHeaders, "content-type": "application/json" } : { ...hmacHeaders };
|
|
577
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
578
|
+
method,
|
|
579
|
+
headers,
|
|
580
|
+
body: method === "POST" ? bodyStr : void 0
|
|
581
|
+
});
|
|
582
|
+
if (response.status === 202) {
|
|
583
|
+
const data = await response.json();
|
|
584
|
+
return { _status: 202, approvalRequestId: data.approvalRequestId };
|
|
585
|
+
}
|
|
586
|
+
if (!response.ok) {
|
|
587
|
+
let code = "UNKNOWN";
|
|
588
|
+
let message = `HTTP ${response.status}`;
|
|
589
|
+
try {
|
|
590
|
+
const data = await response.json();
|
|
591
|
+
code = data.code ?? defaultCodeForStatus(response.status);
|
|
592
|
+
message = data.error ?? message;
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
throw new KeeperHubError(code, message);
|
|
596
|
+
}
|
|
597
|
+
return await response.json();
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
660
601
|
// src/safety-config.ts
|
|
661
602
|
var import_promises3 = require("fs/promises");
|
|
662
603
|
var import_node_os3 = require("os");
|
|
@@ -737,8 +678,6 @@ function getSafetyConfigPath() {
|
|
|
737
678
|
}
|
|
738
679
|
|
|
739
680
|
// src/hook.ts
|
|
740
|
-
var DEFAULT_POLL = { intervalMs: 2e3, maxAttempts: 150 };
|
|
741
|
-
var APPROVAL_URL_BASE = "https://app.keeperhub.com/approve/";
|
|
742
681
|
var USDC_DECIMALS2 = 1e6;
|
|
743
682
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
744
683
|
var MICRO_USDC_RE = /^\d+$/;
|
|
@@ -789,16 +728,6 @@ function usdToMicro(usd) {
|
|
|
789
728
|
async function createPreToolUseHook(options = {}) {
|
|
790
729
|
const toolMatcher = options.toolNameMatcher ?? defaultToolMatcher;
|
|
791
730
|
const configLoader = options.configLoader ?? loadSafetyConfig;
|
|
792
|
-
const walletLoader = options.walletLoader ?? readWalletConfig;
|
|
793
|
-
const clientFactory = options.clientFactory ?? ((w) => new KeeperHubClient(w));
|
|
794
|
-
const onAskOpen = options.onAskOpen ?? ((url) => {
|
|
795
|
-
process.stderr.write(
|
|
796
|
-
`
|
|
797
|
-
[keeperhub-wallet] Approval required. Visit: ${url}
|
|
798
|
-
`
|
|
799
|
-
);
|
|
800
|
-
});
|
|
801
|
-
const poll = options.poll ?? DEFAULT_POLL;
|
|
802
731
|
const safety = await configLoader();
|
|
803
732
|
return async (raw) => {
|
|
804
733
|
const hookInput = raw ?? {};
|
|
@@ -814,43 +743,10 @@ async function createPreToolUseHook(options = {}) {
|
|
|
814
743
|
return { decision: "deny", reason: "AMOUNT_UNDETERMINED" };
|
|
815
744
|
}
|
|
816
745
|
const blockMicro = usdToMicro(safety.block_threshold_usd);
|
|
817
|
-
const askMicro = usdToMicro(safety.ask_threshold_usd);
|
|
818
746
|
const autoMicro = usdToMicro(safety.auto_approve_max_usd);
|
|
819
747
|
if (amountMicro > blockMicro) {
|
|
820
748
|
return { decision: "deny", reason: "BLOCKED_BY_SAFETY_RULE" };
|
|
821
749
|
}
|
|
822
|
-
if (amountMicro >= askMicro) {
|
|
823
|
-
const wallet = await walletLoader();
|
|
824
|
-
const client = clientFactory(wallet);
|
|
825
|
-
const created = await client.request("POST", "/api/agentic-wallet/approval-request", {
|
|
826
|
-
// Server contract (Phase 33): riskLevel MUST be 'ask' or 'block';
|
|
827
|
-
// operationPayload MUST be a non-array object. The hook only creates
|
|
828
|
-
// approval-requests at the ask tier (block tier short-circuits above).
|
|
829
|
-
riskLevel: "ask",
|
|
830
|
-
operationPayload: {
|
|
831
|
-
amountMicroUsdc: amountMicro.toString(),
|
|
832
|
-
contractAddress: contractAddr ?? "",
|
|
833
|
-
toolName: hookInput.tool_name ?? "",
|
|
834
|
-
reason: `Agent tool ${hookInput.tool_name}`
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
const approvalId = "_status" in created ? created.approvalRequestId : created.id;
|
|
838
|
-
onAskOpen(`${APPROVAL_URL_BASE}${approvalId}`);
|
|
839
|
-
for (let attempt = 0; attempt < poll.maxAttempts; attempt++) {
|
|
840
|
-
await new Promise((r) => setTimeout(r, poll.intervalMs));
|
|
841
|
-
const status = await client.request("GET", `/api/agentic-wallet/approval-request/${approvalId}`);
|
|
842
|
-
if (!("status" in status)) {
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
|
-
if (status.status === "approved") {
|
|
846
|
-
return { decision: "allow" };
|
|
847
|
-
}
|
|
848
|
-
if (status.status === "rejected") {
|
|
849
|
-
return { decision: "deny", reason: "USER_REJECTED" };
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return { decision: "deny", reason: "APPROVAL_TIMEOUT" };
|
|
853
|
-
}
|
|
854
750
|
if (amountMicro <= autoMicro) {
|
|
855
751
|
return { decision: "allow" };
|
|
856
752
|
}
|