@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
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
|
|
111
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
373
|
+
process.stdout.write(`Base USDC: ${snap.base.amount}
|
|
522
374
|
`);
|
|
523
|
-
process.stdout.write(`Tempo USDC.e:
|
|
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.
|
|
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
|
}
|