@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/index.d.ts CHANGED
@@ -37,49 +37,6 @@ declare class WalletConfigMissingError extends Error {
37
37
  constructor();
38
38
  }
39
39
 
40
- type ClientOptions = {
41
- /** Defaults to process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com" */
42
- baseUrl?: string;
43
- /** Injected for tests; defaults to global fetch */
44
- fetch?: typeof fetch;
45
- };
46
- /**
47
- * 202 ask-tier envelope returned by /sign and /approval-request when the
48
- * risk classifier routes a request to the ask queue. Callers poll
49
- * `/api/agentic-wallet/approval-request/:id` until status !== "pending".
50
- */
51
- type AskTierResponse = {
52
- _status: 202;
53
- approvalRequestId: string;
54
- };
55
- /**
56
- * HMAC-signed HTTP client for the KeeperHub agentic-wallet API surface.
57
- * Every request to /api/agentic-wallet/* (except /provision, which uses
58
- * the session cookie) flows through this class.
59
- *
60
- * @security No logging of headers, body, or response bodies. Any stdout
61
- * emitter (the global console object or util.inspect) added to this
62
- * file is a T-34-08 violation (grep-enforced in CI).
63
- */
64
- declare class KeeperHubClient {
65
- private readonly baseUrl;
66
- private readonly fetchImpl;
67
- private readonly wallet;
68
- constructor(wallet: WalletConfig, opts?: ClientOptions);
69
- /**
70
- * HMAC-signed POST/GET to any /api/agentic-wallet/* route except
71
- * /provision. Path MUST start with a leading slash. Body is
72
- * JSON.stringify'd (or the empty string for GET).
73
- *
74
- * Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
75
- * message)` where `code` is the server-supplied field or the default
76
- * taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
77
- * `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
78
- * AskTierResponse envelope.
79
- */
80
- request<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T | AskTierResponse>;
81
- }
82
-
83
40
  type BalanceSnapshot = {
84
41
  base: {
85
42
  chain: "base";
@@ -93,23 +50,16 @@ type BalanceSnapshot = {
93
50
  amount: string;
94
51
  address: `0x${string}`;
95
52
  };
96
- offChainCredit: {
97
- amount: string;
98
- currency: "USD";
99
- };
100
53
  };
101
54
  type CheckBalanceOptions = {
102
55
  /** Injectable viem client for Base (tests mock readContract). */
103
56
  baseClient?: PublicClient;
104
57
  /** Injectable viem client for Tempo (tests mock readContract). */
105
58
  tempoClient?: PublicClient;
106
- /** Injectable KeeperHubClient (tests inject a mocked fetch). */
107
- khClient?: KeeperHubClient;
108
59
  };
109
60
  /**
110
- * Read the wallet's balance across Base + Tempo + off-chain KeeperHub credit
111
- * in parallel. All three legs must resolve; any single failure rejects the
112
- * Promise.
61
+ * Read the wallet's on-chain balance across Base + Tempo in parallel. Both
62
+ * legs must resolve; any single failure rejects the Promise.
113
63
  *
114
64
  * Amounts are formatted as decimal strings (6-decimal USDC precision) so the
115
65
  * caller can render them without BigInt math.
@@ -168,6 +118,49 @@ declare const BASE_USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
168
118
  /** Bridged USDC (USDC.e) on Tempo mainnet. NOT the same contract as BASE_USDC. */
169
119
  declare const TEMPO_USDC_E: "0x20c000000000000000000000b9537d11c60e8b50";
170
120
 
121
+ type ClientOptions = {
122
+ /** Defaults to process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com" */
123
+ baseUrl?: string;
124
+ /** Injected for tests; defaults to global fetch */
125
+ fetch?: typeof fetch;
126
+ };
127
+ /**
128
+ * 202 ask-tier envelope returned by /sign and /approval-request when the
129
+ * risk classifier routes a request to the ask queue. Callers poll
130
+ * `/api/agentic-wallet/approval-request/:id` until status !== "pending".
131
+ */
132
+ type AskTierResponse = {
133
+ _status: 202;
134
+ approvalRequestId: string;
135
+ };
136
+ /**
137
+ * HMAC-signed HTTP client for the KeeperHub agentic-wallet API surface.
138
+ * Every request to /api/agentic-wallet/* (except /provision, which uses
139
+ * the session cookie) flows through this class.
140
+ *
141
+ * @security No logging of headers, body, or response bodies. Any stdout
142
+ * emitter (the global console object or util.inspect) added to this
143
+ * file is a T-34-08 violation (grep-enforced in CI).
144
+ */
145
+ declare class KeeperHubClient {
146
+ private readonly baseUrl;
147
+ private readonly fetchImpl;
148
+ private readonly wallet;
149
+ constructor(wallet: WalletConfig, opts?: ClientOptions);
150
+ /**
151
+ * HMAC-signed POST/GET to any /api/agentic-wallet/* route except
152
+ * /provision. Path MUST start with a leading slash. Body is
153
+ * JSON.stringify'd (or the empty string for GET).
154
+ *
155
+ * Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
156
+ * message)` where `code` is the server-supplied field or the default
157
+ * taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
158
+ * `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
159
+ * AskTierResponse envelope.
160
+ */
161
+ request<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T | AskTierResponse>;
162
+ }
163
+
171
164
  type FundInstructions = {
172
165
  /** Coinbase Onramp deeplink (legacy query-param form). */
173
166
  coinbaseOnrampUrl: string;
@@ -235,26 +228,29 @@ type CreateHookOptions = {
235
228
  /** Match against tool_name. Default: /keeperhub|wallet|sign/i */
236
229
  toolNameMatcher?: (name: string) => boolean;
237
230
  /** Injected for tests */
238
- walletLoader?: () => Promise<WalletConfig>;
239
- /** Injected for tests */
240
231
  configLoader?: () => Promise<SafetyConfig>;
241
- /** Injected for tests */
242
- clientFactory?: (w: WalletConfig) => KeeperHubClient;
243
- /**
244
- * Called when the ask tier opens an approval URL. Default: write to stderr
245
- * (stdout is reserved for the Claude Code hook JSON output).
246
- */
247
- onAskOpen?: (url: string) => void;
248
- /** Polling config for the ask tier */
249
- poll?: {
250
- intervalMs: number;
251
- maxAttempts: number;
252
- };
253
232
  };
254
233
  /**
255
- * Factory returning the PreToolUse hook function. The hook enforces the three
234
+ * Factory returning the PreToolUse hook function. The hook enforces three
256
235
  * client-side safety tiers (auto / ask / block) sourced EXCLUSIVELY from
257
236
  * ~/.keeperhub/safety.json -- never from the tool payload (GUARD-05).
237
+ *
238
+ * v0.1.4 collapsed the previous four-band behaviour into three:
239
+ *
240
+ * amount <= auto_approve_max_usd -> {decision: "allow"}
241
+ * auto_approve_max_usd < amount <= block_threshold -> {decision: "ask"} (Claude Code prompts user inline)
242
+ * amount > block_threshold -> {decision: "deny"}
243
+ *
244
+ * The previous server-approval branch (amount >= ask_threshold -> create a
245
+ * /api/agentic-wallet/approval-request row, print an approval URL, poll for
246
+ * browser approval) was removed. It required the wallet to be linked to a
247
+ * KeeperHub user via /link, and the link command was rough enough that we
248
+ * never wired it into the documented flow. Returning {decision: "ask"}
249
+ * inline lets Claude Code surface the prompt in the agent chat directly.
250
+ *
251
+ * `ask_threshold_usd` is still read from safety.json for backward-compat
252
+ * with existing configs but is not consulted for decision-making. Tracked
253
+ * as KEEP-307 for the permanent architectural decision.
258
254
  */
259
255
  declare function createPreToolUseHook(options?: CreateHookOptions): Promise<(input: unknown) => Promise<HookDecision>>;
260
256
 
package/dist/index.js CHANGED
@@ -79,124 +79,6 @@ var tempo = defineChain({
79
79
  var BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
80
80
  var TEMPO_USDC_E = "0x20c000000000000000000000b9537d11c60e8b50";
81
81
 
82
- // src/hmac.ts
83
- import { createHash, createHmac } from "crypto";
84
- function computeSignature(secret, method, path, subOrgId, body, timestamp) {
85
- const bodyDigest = createHash("sha256").update(body).digest("hex");
86
- const signingString = `${method}
87
- ${path}
88
- ${subOrgId}
89
- ${bodyDigest}
90
- ${timestamp}`;
91
- return createHmac("sha256", secret).update(signingString).digest("hex");
92
- }
93
- function buildHmacHeaders(secret, method, path, subOrgId, body) {
94
- const timestamp = String(Math.floor(Date.now() / 1e3));
95
- const signature = computeSignature(
96
- secret,
97
- method,
98
- path,
99
- subOrgId,
100
- body,
101
- timestamp
102
- );
103
- return {
104
- "X-KH-Sub-Org": subOrgId,
105
- "X-KH-Timestamp": timestamp,
106
- "X-KH-Signature": signature
107
- };
108
- }
109
-
110
- // src/types.ts
111
- var KeeperHubError = class extends Error {
112
- code;
113
- constructor(code, message) {
114
- super(message);
115
- this.name = "KeeperHubError";
116
- this.code = code;
117
- }
118
- };
119
- var WalletConfigMissingError = class extends Error {
120
- constructor() {
121
- super(
122
- "Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
123
- );
124
- this.name = "WalletConfigMissingError";
125
- }
126
- };
127
-
128
- // src/client.ts
129
- var TRAILING_SLASH = /\/$/;
130
- function defaultCodeForStatus(status) {
131
- if (status === 401) {
132
- return "HMAC_INVALID";
133
- }
134
- if (status === 403) {
135
- return "POLICY_BLOCKED";
136
- }
137
- if (status === 404) {
138
- return "NOT_FOUND";
139
- }
140
- if (status === 502) {
141
- return "TURNKEY_UPSTREAM";
142
- }
143
- return `HTTP_${status}`;
144
- }
145
- var KeeperHubClient = class {
146
- baseUrl;
147
- fetchImpl;
148
- wallet;
149
- constructor(wallet, opts = {}) {
150
- this.wallet = wallet;
151
- const envBase = process.env.KEEPERHUB_API_URL;
152
- this.baseUrl = (opts.baseUrl ?? envBase ?? "https://app.keeperhub.com").replace(TRAILING_SLASH, "");
153
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
154
- }
155
- /**
156
- * HMAC-signed POST/GET to any /api/agentic-wallet/* route except
157
- * /provision. Path MUST start with a leading slash. Body is
158
- * JSON.stringify'd (or the empty string for GET).
159
- *
160
- * Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
161
- * message)` where `code` is the server-supplied field or the default
162
- * taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
163
- * `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
164
- * AskTierResponse envelope.
165
- */
166
- async request(method, path, body) {
167
- const bodyStr = body === void 0 ? "" : JSON.stringify(body);
168
- const hmacHeaders = buildHmacHeaders(
169
- this.wallet.hmacSecret,
170
- method,
171
- path,
172
- this.wallet.subOrgId,
173
- bodyStr
174
- );
175
- const headers = method === "POST" ? { ...hmacHeaders, "content-type": "application/json" } : { ...hmacHeaders };
176
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
177
- method,
178
- headers,
179
- body: method === "POST" ? bodyStr : void 0
180
- });
181
- if (response.status === 202) {
182
- const data = await response.json();
183
- return { _status: 202, approvalRequestId: data.approvalRequestId };
184
- }
185
- if (!response.ok) {
186
- let code = "UNKNOWN";
187
- let message = `HTTP ${response.status}`;
188
- try {
189
- const data = await response.json();
190
- code = data.code ?? defaultCodeForStatus(response.status);
191
- message = data.error ?? message;
192
- } catch {
193
- }
194
- throw new KeeperHubError(code, message);
195
- }
196
- return await response.json();
197
- }
198
- };
199
-
200
82
  // src/balance.ts
201
83
  var USDC_DECIMALS = 6;
202
84
  async function checkBalance(wallet, opts = {}) {
@@ -208,8 +90,7 @@ async function checkBalance(wallet, opts = {}) {
208
90
  chain: tempo,
209
91
  transport: http()
210
92
  });
211
- const khClient = opts.khClient ?? new KeeperHubClient(wallet);
212
- const [baseRaw, tempoRaw, credit] = await Promise.all([
93
+ const [baseRaw, tempoRaw] = await Promise.all([
213
94
  baseClient.readContract({
214
95
  address: BASE_USDC,
215
96
  abi: erc20Abi,
@@ -221,12 +102,8 @@ async function checkBalance(wallet, opts = {}) {
221
102
  abi: erc20Abi,
222
103
  functionName: "balanceOf",
223
104
  args: [wallet.walletAddress]
224
- }),
225
- khClient.request("GET", "/api/agentic-wallet/credit")
105
+ })
226
106
  ]);
227
- if ("_status" in credit) {
228
- throw new Error("Unexpected 202 response from /api/agentic-wallet/credit");
229
- }
230
107
  return {
231
108
  base: {
232
109
  chain: "base",
@@ -239,10 +116,6 @@ async function checkBalance(wallet, opts = {}) {
239
116
  token: "USDC.e",
240
117
  amount: formatUnits(tempoRaw, USDC_DECIMALS),
241
118
  address: wallet.walletAddress
242
- },
243
- offChainCredit: {
244
- amount: credit.amount,
245
- currency: "USD"
246
119
  }
247
120
  };
248
121
  }
@@ -372,6 +245,26 @@ async function installSkill(options = {}) {
372
245
  import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
373
246
  import { homedir as homedir2 } from "os";
374
247
  import { dirname as dirname3, join as join3 } from "path";
248
+
249
+ // src/types.ts
250
+ var KeeperHubError = class extends Error {
251
+ code;
252
+ constructor(code, message) {
253
+ super(message);
254
+ this.name = "KeeperHubError";
255
+ this.code = code;
256
+ }
257
+ };
258
+ var WalletConfigMissingError = class extends Error {
259
+ constructor() {
260
+ super(
261
+ "Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision."
262
+ );
263
+ this.name = "WalletConfigMissingError";
264
+ }
265
+ };
266
+
267
+ // src/storage.ts
375
268
  async function readWalletConfig() {
376
269
  const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
377
270
  let raw;
@@ -400,11 +293,11 @@ function getWalletConfigPath() {
400
293
  }
401
294
 
402
295
  // src/cli.ts
403
- var TRAILING_SLASH2 = /\/$/;
296
+ var TRAILING_SLASH = /\/$/;
404
297
  var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
405
298
  function resolveBaseUrl(override) {
406
299
  const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
407
- return candidate.replace(TRAILING_SLASH2, "");
300
+ return candidate.replace(TRAILING_SLASH, "");
408
301
  }
409
302
  function isNonEmptyString(value) {
410
303
  return typeof value === "string" && value.length > 0;
@@ -464,47 +357,6 @@ async function cmdAdd(opts = {}) {
464
357
  process.stdout.write(`config written to ${getWalletConfigPath()}
465
358
  `);
466
359
  }
467
- async function cmdLink(opts = {}) {
468
- const wallet = await readWalletConfig();
469
- const baseUrl = resolveBaseUrl(opts.baseUrl);
470
- const sessionCookie = process.env.KH_SESSION_COOKIE;
471
- if (!sessionCookie) {
472
- process.stderr.write(
473
- "[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"
474
- );
475
- process.exit(1);
476
- }
477
- const body = JSON.stringify({ subOrgId: wallet.subOrgId });
478
- const headers = buildHmacHeaders(
479
- wallet.hmacSecret,
480
- "POST",
481
- "/api/agentic-wallet/link",
482
- wallet.subOrgId,
483
- body
484
- );
485
- const response = await fetch(`${baseUrl}/api/agentic-wallet/link`, {
486
- method: "POST",
487
- headers: {
488
- ...headers,
489
- "content-type": "application/json",
490
- cookie: sessionCookie
491
- },
492
- body
493
- });
494
- const json = await response.json().catch(() => ({}));
495
- if (!response.ok) {
496
- process.stderr.write(
497
- `[keeperhub-wallet] link failed: ${json.code ?? response.status}: ${json.error ?? ""}
498
- `
499
- );
500
- process.exit(1);
501
- }
502
- if (json.already) {
503
- process.stdout.write("already linked\n");
504
- return;
505
- }
506
- process.stdout.write("linked\n");
507
- }
508
360
  async function cmdFund() {
509
361
  const wallet = await readWalletConfig();
510
362
  const out = fund(wallet.walletAddress);
@@ -518,14 +370,10 @@ async function cmdFund() {
518
370
  async function cmdBalance() {
519
371
  const wallet = await readWalletConfig();
520
372
  const snap = await checkBalance(wallet);
521
- process.stdout.write(`Base USDC: ${snap.base.amount}
373
+ process.stdout.write(`Base USDC: ${snap.base.amount}
522
374
  `);
523
- process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}
375
+ process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}
524
376
  `);
525
- process.stdout.write(
526
- `KeeperHub credit: ${snap.offChainCredit.amount} ${snap.offChainCredit.currency}
527
- `
528
- );
529
377
  }
530
378
  async function cmdInfo() {
531
379
  const wallet = await readWalletConfig();
@@ -538,23 +386,16 @@ async function runCli(argv = process.argv) {
538
386
  const program = new Command();
539
387
  program.name("keeperhub-wallet").description(
540
388
  "KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)"
541
- ).version("0.1.0");
389
+ ).version("0.1.3");
542
390
  program.command("add").description("Provision a new agentic wallet (no account required)").option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
543
391
  await cmdAdd(opts);
544
392
  });
545
- program.command("link").description(
546
- "Link the current wallet to your KeeperHub account (requires KH_SESSION_COOKIE env)"
547
- ).option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
548
- await cmdLink(opts);
549
- });
550
393
  program.command("fund").description(
551
394
  "Print Coinbase Onramp URL (Base USDC) and Tempo deposit address"
552
395
  ).action(async () => {
553
396
  await cmdFund();
554
397
  });
555
- program.command("balance").description(
556
- "Print unified balance: Base USDC + Tempo USDC.e + off-chain KeeperHub credit"
557
- ).action(async () => {
398
+ program.command("balance").description("Print on-chain balance: Base USDC + Tempo USDC.e").action(async () => {
558
399
  await cmdBalance();
559
400
  });
560
401
  program.command("info").description("Print subOrgId and walletAddress from local config").action(async () => {
@@ -609,6 +450,106 @@ async function runCli(argv = process.argv) {
609
450
  }
610
451
  }
611
452
 
453
+ // src/hmac.ts
454
+ import { createHash, createHmac } from "crypto";
455
+ function computeSignature(secret, method, path, subOrgId, body, timestamp) {
456
+ const bodyDigest = createHash("sha256").update(body).digest("hex");
457
+ const signingString = `${method}
458
+ ${path}
459
+ ${subOrgId}
460
+ ${bodyDigest}
461
+ ${timestamp}`;
462
+ return createHmac("sha256", secret).update(signingString).digest("hex");
463
+ }
464
+ function buildHmacHeaders(secret, method, path, subOrgId, body) {
465
+ const timestamp = String(Math.floor(Date.now() / 1e3));
466
+ const signature = computeSignature(
467
+ secret,
468
+ method,
469
+ path,
470
+ subOrgId,
471
+ body,
472
+ timestamp
473
+ );
474
+ return {
475
+ "X-KH-Sub-Org": subOrgId,
476
+ "X-KH-Timestamp": timestamp,
477
+ "X-KH-Signature": signature
478
+ };
479
+ }
480
+
481
+ // src/client.ts
482
+ var TRAILING_SLASH2 = /\/$/;
483
+ function defaultCodeForStatus(status) {
484
+ if (status === 401) {
485
+ return "HMAC_INVALID";
486
+ }
487
+ if (status === 403) {
488
+ return "POLICY_BLOCKED";
489
+ }
490
+ if (status === 404) {
491
+ return "NOT_FOUND";
492
+ }
493
+ if (status === 502) {
494
+ return "TURNKEY_UPSTREAM";
495
+ }
496
+ return `HTTP_${status}`;
497
+ }
498
+ var KeeperHubClient = class {
499
+ baseUrl;
500
+ fetchImpl;
501
+ wallet;
502
+ constructor(wallet, opts = {}) {
503
+ this.wallet = wallet;
504
+ const envBase = process.env.KEEPERHUB_API_URL;
505
+ this.baseUrl = (opts.baseUrl ?? envBase ?? "https://app.keeperhub.com").replace(TRAILING_SLASH2, "");
506
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
507
+ }
508
+ /**
509
+ * HMAC-signed POST/GET to any /api/agentic-wallet/* route except
510
+ * /provision. Path MUST start with a leading slash. Body is
511
+ * JSON.stringify'd (or the empty string for GET).
512
+ *
513
+ * Error mapping: non-2xx/non-202 surface as `KeeperHubError(code,
514
+ * message)` where `code` is the server-supplied field or the default
515
+ * taxonomy (`HMAC_INVALID`, `POLICY_BLOCKED`, `NOT_FOUND`,
516
+ * `TURNKEY_UPSTREAM`, `HTTP_<status>`). 202 ask-tier surfaces as an
517
+ * AskTierResponse envelope.
518
+ */
519
+ async request(method, path, body) {
520
+ const bodyStr = body === void 0 ? "" : JSON.stringify(body);
521
+ const hmacHeaders = buildHmacHeaders(
522
+ this.wallet.hmacSecret,
523
+ method,
524
+ path,
525
+ this.wallet.subOrgId,
526
+ bodyStr
527
+ );
528
+ const headers = method === "POST" ? { ...hmacHeaders, "content-type": "application/json" } : { ...hmacHeaders };
529
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
530
+ method,
531
+ headers,
532
+ body: method === "POST" ? bodyStr : void 0
533
+ });
534
+ if (response.status === 202) {
535
+ const data = await response.json();
536
+ return { _status: 202, approvalRequestId: data.approvalRequestId };
537
+ }
538
+ if (!response.ok) {
539
+ let code = "UNKNOWN";
540
+ let message = `HTTP ${response.status}`;
541
+ try {
542
+ const data = await response.json();
543
+ code = data.code ?? defaultCodeForStatus(response.status);
544
+ message = data.error ?? message;
545
+ } catch {
546
+ }
547
+ throw new KeeperHubError(code, message);
548
+ }
549
+ return await response.json();
550
+ }
551
+ };
552
+
612
553
  // src/safety-config.ts
613
554
  import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
614
555
  import { homedir as homedir3 } from "os";
@@ -689,8 +630,6 @@ function getSafetyConfigPath() {
689
630
  }
690
631
 
691
632
  // src/hook.ts
692
- var DEFAULT_POLL = { intervalMs: 2e3, maxAttempts: 150 };
693
- var APPROVAL_URL_BASE = "https://app.keeperhub.com/approve/";
694
633
  var USDC_DECIMALS2 = 1e6;
695
634
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
696
635
  var MICRO_USDC_RE = /^\d+$/;
@@ -741,16 +680,6 @@ function usdToMicro(usd) {
741
680
  async function createPreToolUseHook(options = {}) {
742
681
  const toolMatcher = options.toolNameMatcher ?? defaultToolMatcher;
743
682
  const configLoader = options.configLoader ?? loadSafetyConfig;
744
- const walletLoader = options.walletLoader ?? readWalletConfig;
745
- const clientFactory = options.clientFactory ?? ((w) => new KeeperHubClient(w));
746
- const onAskOpen = options.onAskOpen ?? ((url) => {
747
- process.stderr.write(
748
- `
749
- [keeperhub-wallet] Approval required. Visit: ${url}
750
- `
751
- );
752
- });
753
- const poll = options.poll ?? DEFAULT_POLL;
754
683
  const safety = await configLoader();
755
684
  return async (raw) => {
756
685
  const hookInput = raw ?? {};
@@ -766,43 +695,10 @@ async function createPreToolUseHook(options = {}) {
766
695
  return { decision: "deny", reason: "AMOUNT_UNDETERMINED" };
767
696
  }
768
697
  const blockMicro = usdToMicro(safety.block_threshold_usd);
769
- const askMicro = usdToMicro(safety.ask_threshold_usd);
770
698
  const autoMicro = usdToMicro(safety.auto_approve_max_usd);
771
699
  if (amountMicro > blockMicro) {
772
700
  return { decision: "deny", reason: "BLOCKED_BY_SAFETY_RULE" };
773
701
  }
774
- if (amountMicro >= askMicro) {
775
- const wallet = await walletLoader();
776
- const client = clientFactory(wallet);
777
- const created = await client.request("POST", "/api/agentic-wallet/approval-request", {
778
- // Server contract (Phase 33): riskLevel MUST be 'ask' or 'block';
779
- // operationPayload MUST be a non-array object. The hook only creates
780
- // approval-requests at the ask tier (block tier short-circuits above).
781
- riskLevel: "ask",
782
- operationPayload: {
783
- amountMicroUsdc: amountMicro.toString(),
784
- contractAddress: contractAddr ?? "",
785
- toolName: hookInput.tool_name ?? "",
786
- reason: `Agent tool ${hookInput.tool_name}`
787
- }
788
- });
789
- const approvalId = "_status" in created ? created.approvalRequestId : created.id;
790
- onAskOpen(`${APPROVAL_URL_BASE}${approvalId}`);
791
- for (let attempt = 0; attempt < poll.maxAttempts; attempt++) {
792
- await new Promise((r) => setTimeout(r, poll.intervalMs));
793
- const status = await client.request("GET", `/api/agentic-wallet/approval-request/${approvalId}`);
794
- if (!("status" in status)) {
795
- continue;
796
- }
797
- if (status.status === "approved") {
798
- return { decision: "allow" };
799
- }
800
- if (status.status === "rejected") {
801
- return { decision: "deny", reason: "USER_REJECTED" };
802
- }
803
- }
804
- return { decision: "deny", reason: "APPROVAL_TIMEOUT" };
805
- }
806
702
  if (amountMicro <= autoMicro) {
807
703
  return { decision: "allow" };
808
704
  }