@keeperhub/wallet 0.1.0
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/LICENSE +19 -0
- package/README.md +23 -0
- package/bin/keeperhub-wallet-hook.js +9 -0
- package/bin/keeperhub-wallet.js +7 -0
- package/dist/cli.cjs +636 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +3 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/hook-entrypoint.cjs +390 -0
- package/dist/hook-entrypoint.cjs.map +1 -0
- package/dist/hook-entrypoint.d.cts +17 -0
- package/dist/hook-entrypoint.d.ts +17 -0
- package/dist/hook-entrypoint.js +363 -0
- package/dist/hook-entrypoint.js.map +1 -0
- package/dist/index.cjs +1114 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +1065 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/skill/keeperhub-wallet.skill.md +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
// src/agent-detect.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
var AGENT_SPECS = [
|
|
6
|
+
{
|
|
7
|
+
agent: "claude-code",
|
|
8
|
+
skillsRel: [".claude", "skills"],
|
|
9
|
+
settingsRel: [".claude", "settings.json"],
|
|
10
|
+
hookSupport: "claude-code"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
agent: "cursor",
|
|
14
|
+
skillsRel: [".cursor", "skills"],
|
|
15
|
+
settingsRel: [".cursor", "settings.json"],
|
|
16
|
+
hookSupport: "notice"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
agent: "cline",
|
|
20
|
+
skillsRel: [".cline", "skills"],
|
|
21
|
+
settingsRel: [".cline", "settings.json"],
|
|
22
|
+
hookSupport: "notice"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
agent: "windsurf",
|
|
26
|
+
skillsRel: [".windsurf", "skills"],
|
|
27
|
+
settingsRel: [".windsurf", "settings.json"],
|
|
28
|
+
hookSupport: "notice"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
agent: "opencode",
|
|
32
|
+
skillsRel: [".config", "opencode", "skills"],
|
|
33
|
+
settingsRel: [".config", "opencode", "settings.json"],
|
|
34
|
+
hookSupport: "notice"
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
function detectAgents(homeOverride) {
|
|
38
|
+
const home = homeOverride ?? homedir();
|
|
39
|
+
const results = [];
|
|
40
|
+
for (const spec of AGENT_SPECS) {
|
|
41
|
+
const skillsDir = join(home, ...spec.skillsRel);
|
|
42
|
+
const settingsFile = join(home, ...spec.settingsRel);
|
|
43
|
+
if (existsSync(dirname(skillsDir))) {
|
|
44
|
+
results.push({
|
|
45
|
+
agent: spec.agent,
|
|
46
|
+
skillsDir,
|
|
47
|
+
settingsFile,
|
|
48
|
+
hookSupport: spec.hookSupport
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/balance.ts
|
|
56
|
+
import {
|
|
57
|
+
createPublicClient,
|
|
58
|
+
erc20Abi,
|
|
59
|
+
formatUnits,
|
|
60
|
+
http
|
|
61
|
+
} from "viem";
|
|
62
|
+
|
|
63
|
+
// src/chains.ts
|
|
64
|
+
import { defineChain } from "viem";
|
|
65
|
+
import { base } from "viem/chains";
|
|
66
|
+
var tempo = defineChain({
|
|
67
|
+
id: 4217,
|
|
68
|
+
name: "Tempo",
|
|
69
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
70
|
+
rpcUrls: {
|
|
71
|
+
default: {
|
|
72
|
+
http: [process.env.TEMPO_RPC_URL ?? "https://rpc.tempo.xyz"]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
blockExplorers: {
|
|
76
|
+
default: { name: "Tempo Explorer", url: "https://explorer.tempo.xyz" }
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
var BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
80
|
+
var TEMPO_USDC_E = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
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
|
+
// src/balance.ts
|
|
201
|
+
var USDC_DECIMALS = 6;
|
|
202
|
+
async function checkBalance(wallet, opts = {}) {
|
|
203
|
+
const baseClient = opts.baseClient ?? createPublicClient({
|
|
204
|
+
chain: base,
|
|
205
|
+
transport: http()
|
|
206
|
+
});
|
|
207
|
+
const tempoClient = opts.tempoClient ?? createPublicClient({
|
|
208
|
+
chain: tempo,
|
|
209
|
+
transport: http()
|
|
210
|
+
});
|
|
211
|
+
const khClient = opts.khClient ?? new KeeperHubClient(wallet);
|
|
212
|
+
const [baseRaw, tempoRaw, credit] = await Promise.all([
|
|
213
|
+
baseClient.readContract({
|
|
214
|
+
address: BASE_USDC,
|
|
215
|
+
abi: erc20Abi,
|
|
216
|
+
functionName: "balanceOf",
|
|
217
|
+
args: [wallet.walletAddress]
|
|
218
|
+
}),
|
|
219
|
+
tempoClient.readContract({
|
|
220
|
+
address: TEMPO_USDC_E,
|
|
221
|
+
abi: erc20Abi,
|
|
222
|
+
functionName: "balanceOf",
|
|
223
|
+
args: [wallet.walletAddress]
|
|
224
|
+
}),
|
|
225
|
+
khClient.request("GET", "/api/agentic-wallet/credit")
|
|
226
|
+
]);
|
|
227
|
+
if ("_status" in credit) {
|
|
228
|
+
throw new Error("Unexpected 202 response from /api/agentic-wallet/credit");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
base: {
|
|
232
|
+
chain: "base",
|
|
233
|
+
token: "USDC",
|
|
234
|
+
amount: formatUnits(baseRaw, USDC_DECIMALS),
|
|
235
|
+
address: wallet.walletAddress
|
|
236
|
+
},
|
|
237
|
+
tempo: {
|
|
238
|
+
chain: "tempo",
|
|
239
|
+
token: "USDC.e",
|
|
240
|
+
amount: formatUnits(tempoRaw, USDC_DECIMALS),
|
|
241
|
+
address: wallet.walletAddress
|
|
242
|
+
},
|
|
243
|
+
offChainCredit: {
|
|
244
|
+
amount: credit.amount,
|
|
245
|
+
currency: "USD"
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/cli.ts
|
|
251
|
+
import { Command } from "commander";
|
|
252
|
+
|
|
253
|
+
// src/fund.ts
|
|
254
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
255
|
+
var COINBASE_HOST = "pay.coinbase.com";
|
|
256
|
+
var COINBASE_PATH = "/buy/select-asset";
|
|
257
|
+
function fund(walletAddress) {
|
|
258
|
+
if (!EVM_ADDRESS_RE.test(walletAddress)) {
|
|
259
|
+
throw new Error(`Invalid EVM wallet address: ${walletAddress}`);
|
|
260
|
+
}
|
|
261
|
+
const params = new URLSearchParams({
|
|
262
|
+
defaultNetwork: "base",
|
|
263
|
+
defaultAsset: "USDC",
|
|
264
|
+
addresses: JSON.stringify({ [walletAddress]: ["base"] }),
|
|
265
|
+
presetCryptoAmount: "5"
|
|
266
|
+
});
|
|
267
|
+
const coinbaseOnrampUrl = `https://${COINBASE_HOST}${COINBASE_PATH}?${params.toString()}`;
|
|
268
|
+
const disclaimer = "If the Coinbase page does not pre-fill, paste your address manually. For Tempo USDC.e, transfer from an exchange or another wallet to the address above -- Onramp does not support Tempo directly. Coinbase sessionToken URLs are the 2025+ canonical form; legacy query-param URLs may drop prefill on some accounts.";
|
|
269
|
+
return {
|
|
270
|
+
coinbaseOnrampUrl,
|
|
271
|
+
tempoAddress: walletAddress,
|
|
272
|
+
disclaimer
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/skill-install.ts
|
|
277
|
+
import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
278
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
279
|
+
import { fileURLToPath } from "url";
|
|
280
|
+
var HOOK_COMMAND = "keeperhub-wallet-hook";
|
|
281
|
+
var KEEPERHUB_HOOK_MARKER = "keeperhub-wallet-hook";
|
|
282
|
+
function buildKeeperhubEntry() {
|
|
283
|
+
return {
|
|
284
|
+
matcher: "*",
|
|
285
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function resolveDefaultSkillSource() {
|
|
289
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
290
|
+
return join2(here, "..", "skill", "keeperhub-wallet.skill.md");
|
|
291
|
+
}
|
|
292
|
+
function defaultNotice(msg) {
|
|
293
|
+
process.stderr.write(`${msg}
|
|
294
|
+
`);
|
|
295
|
+
}
|
|
296
|
+
async function registerClaudeCodeHook(settingsPath) {
|
|
297
|
+
let raw = null;
|
|
298
|
+
try {
|
|
299
|
+
raw = await readFile(settingsPath, "utf-8");
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (err.code !== "ENOENT") {
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
let config = {};
|
|
306
|
+
if (raw !== null) {
|
|
307
|
+
try {
|
|
308
|
+
config = JSON.parse(raw);
|
|
309
|
+
} catch {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`settings.json at ${settingsPath} is not valid JSON; aborting hook registration`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const hooks = typeof config.hooks === "object" && config.hooks !== null ? config.hooks : {};
|
|
316
|
+
const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
317
|
+
const filtered = [];
|
|
318
|
+
for (const entry of existingPreToolUse) {
|
|
319
|
+
const serialised = JSON.stringify(entry);
|
|
320
|
+
if (!serialised.includes(KEEPERHUB_HOOK_MARKER)) {
|
|
321
|
+
filtered.push(entry);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
filtered.push(buildKeeperhubEntry());
|
|
325
|
+
hooks.PreToolUse = filtered;
|
|
326
|
+
config.hooks = hooks;
|
|
327
|
+
await mkdir(dirname2(settingsPath), { recursive: true, mode: 448 });
|
|
328
|
+
const payload = `${JSON.stringify(config, null, 2)}
|
|
329
|
+
`;
|
|
330
|
+
await writeFile(settingsPath, payload, { mode: 384 });
|
|
331
|
+
await chmod(settingsPath, 384);
|
|
332
|
+
}
|
|
333
|
+
async function writeSkillToAgent(agent, skillSource) {
|
|
334
|
+
await mkdir(agent.skillsDir, { recursive: true, mode: 493 });
|
|
335
|
+
const target = join2(agent.skillsDir, "keeperhub-wallet.skill.md");
|
|
336
|
+
await copyFile(skillSource, target);
|
|
337
|
+
await chmod(target, 420);
|
|
338
|
+
return { agent: agent.agent, path: target, status: "written" };
|
|
339
|
+
}
|
|
340
|
+
function buildNoticeMessage(agent) {
|
|
341
|
+
return `${agent.agent} does not support auto-registered PreToolUse hooks; run \`${HOOK_COMMAND}\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;
|
|
342
|
+
}
|
|
343
|
+
async function installSkill(options = {}) {
|
|
344
|
+
const agents = detectAgents(options.homeOverride);
|
|
345
|
+
const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();
|
|
346
|
+
const onNotice = options.onNotice ?? defaultNotice;
|
|
347
|
+
const skillWrites = [];
|
|
348
|
+
const hookRegistrations = [];
|
|
349
|
+
for (const agent of agents) {
|
|
350
|
+
const write = await writeSkillToAgent(agent, skillSource);
|
|
351
|
+
skillWrites.push(write);
|
|
352
|
+
if (agent.hookSupport === "claude-code") {
|
|
353
|
+
await registerClaudeCodeHook(agent.settingsFile);
|
|
354
|
+
hookRegistrations.push({
|
|
355
|
+
agent: agent.agent,
|
|
356
|
+
status: "registered"
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
const message = buildNoticeMessage(agent);
|
|
360
|
+
hookRegistrations.push({
|
|
361
|
+
agent: agent.agent,
|
|
362
|
+
status: "notice",
|
|
363
|
+
message
|
|
364
|
+
});
|
|
365
|
+
onNotice(message);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { skillWrites, hookRegistrations };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/storage.ts
|
|
372
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
373
|
+
import { homedir as homedir2 } from "os";
|
|
374
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
375
|
+
async function readWalletConfig() {
|
|
376
|
+
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
377
|
+
let raw;
|
|
378
|
+
try {
|
|
379
|
+
raw = await readFile2(walletPath, "utf-8");
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (err.code === "ENOENT") {
|
|
382
|
+
throw new WalletConfigMissingError();
|
|
383
|
+
}
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
const parsed = JSON.parse(raw);
|
|
387
|
+
if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {
|
|
388
|
+
throw new Error(`Malformed wallet.json at ${walletPath}`);
|
|
389
|
+
}
|
|
390
|
+
return parsed;
|
|
391
|
+
}
|
|
392
|
+
async function writeWalletConfig(config) {
|
|
393
|
+
const walletPath = join3(homedir2(), ".keeperhub", "wallet.json");
|
|
394
|
+
await mkdir2(dirname3(walletPath), { recursive: true, mode: 448 });
|
|
395
|
+
await writeFile2(walletPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
396
|
+
await chmod2(walletPath, 384);
|
|
397
|
+
}
|
|
398
|
+
function getWalletConfigPath() {
|
|
399
|
+
return join3(homedir2(), ".keeperhub", "wallet.json");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/cli.ts
|
|
403
|
+
var TRAILING_SLASH2 = /\/$/;
|
|
404
|
+
var WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
|
|
405
|
+
function resolveBaseUrl(override) {
|
|
406
|
+
const candidate = override ?? process.env.KEEPERHUB_API_URL ?? "https://app.keeperhub.com";
|
|
407
|
+
return candidate.replace(TRAILING_SLASH2, "");
|
|
408
|
+
}
|
|
409
|
+
function isNonEmptyString(value) {
|
|
410
|
+
return typeof value === "string" && value.length > 0;
|
|
411
|
+
}
|
|
412
|
+
function provisionInvalidError(message) {
|
|
413
|
+
const err = new Error(message);
|
|
414
|
+
err.code = "PROVISION_RESPONSE_INVALID";
|
|
415
|
+
return err;
|
|
416
|
+
}
|
|
417
|
+
function validateProvisionResponse(data) {
|
|
418
|
+
if (typeof data !== "object" || data === null) {
|
|
419
|
+
throw provisionInvalidError("provision response is not an object");
|
|
420
|
+
}
|
|
421
|
+
const { subOrgId, walletAddress, hmacSecret } = data;
|
|
422
|
+
if (!(isNonEmptyString(subOrgId) && isNonEmptyString(walletAddress) && isNonEmptyString(hmacSecret))) {
|
|
423
|
+
throw provisionInvalidError(
|
|
424
|
+
"provision response missing subOrgId, walletAddress, or hmacSecret"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {
|
|
428
|
+
throw provisionInvalidError(
|
|
429
|
+
`provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
subOrgId,
|
|
434
|
+
walletAddress,
|
|
435
|
+
hmacSecret
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function cmdAdd(opts = {}) {
|
|
439
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
440
|
+
const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { "content-type": "application/json" },
|
|
443
|
+
body: "{}"
|
|
444
|
+
});
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
const text = await response.text();
|
|
447
|
+
process.stderr.write(
|
|
448
|
+
`[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}
|
|
449
|
+
`
|
|
450
|
+
);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
const raw = await response.json();
|
|
454
|
+
const data = validateProvisionResponse(raw);
|
|
455
|
+
await writeWalletConfig({
|
|
456
|
+
subOrgId: data.subOrgId,
|
|
457
|
+
walletAddress: data.walletAddress,
|
|
458
|
+
hmacSecret: data.hmacSecret
|
|
459
|
+
});
|
|
460
|
+
process.stdout.write(`subOrgId: ${data.subOrgId}
|
|
461
|
+
`);
|
|
462
|
+
process.stdout.write(`walletAddress: ${data.walletAddress}
|
|
463
|
+
`);
|
|
464
|
+
process.stdout.write(`config written to ${getWalletConfigPath()}
|
|
465
|
+
`);
|
|
466
|
+
}
|
|
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
|
+
async function cmdFund() {
|
|
509
|
+
const wallet = await readWalletConfig();
|
|
510
|
+
const out = fund(wallet.walletAddress);
|
|
511
|
+
process.stdout.write(`${out.coinbaseOnrampUrl}
|
|
512
|
+
`);
|
|
513
|
+
process.stdout.write(`Tempo address: ${out.tempoAddress}
|
|
514
|
+
`);
|
|
515
|
+
process.stdout.write(`${out.disclaimer}
|
|
516
|
+
`);
|
|
517
|
+
}
|
|
518
|
+
async function cmdBalance() {
|
|
519
|
+
const wallet = await readWalletConfig();
|
|
520
|
+
const snap = await checkBalance(wallet);
|
|
521
|
+
process.stdout.write(`Base USDC: ${snap.base.amount}
|
|
522
|
+
`);
|
|
523
|
+
process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}
|
|
524
|
+
`);
|
|
525
|
+
process.stdout.write(
|
|
526
|
+
`KeeperHub credit: ${snap.offChainCredit.amount} ${snap.offChainCredit.currency}
|
|
527
|
+
`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
async function cmdInfo() {
|
|
531
|
+
const wallet = await readWalletConfig();
|
|
532
|
+
process.stdout.write(`subOrgId: ${wallet.subOrgId}
|
|
533
|
+
`);
|
|
534
|
+
process.stdout.write(`walletAddress: ${wallet.walletAddress}
|
|
535
|
+
`);
|
|
536
|
+
}
|
|
537
|
+
async function runCli(argv = process.argv) {
|
|
538
|
+
const program = new Command();
|
|
539
|
+
program.name("keeperhub-wallet").description(
|
|
540
|
+
"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)"
|
|
541
|
+
).version("0.1.0");
|
|
542
|
+
program.command("add").description("Provision a new agentic wallet (no account required)").option("--base-url <url>", "KeeperHub API base URL").action(async (opts) => {
|
|
543
|
+
await cmdAdd(opts);
|
|
544
|
+
});
|
|
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
|
+
program.command("fund").description(
|
|
551
|
+
"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address"
|
|
552
|
+
).action(async () => {
|
|
553
|
+
await cmdFund();
|
|
554
|
+
});
|
|
555
|
+
program.command("balance").description(
|
|
556
|
+
"Print unified balance: Base USDC + Tempo USDC.e + off-chain KeeperHub credit"
|
|
557
|
+
).action(async () => {
|
|
558
|
+
await cmdBalance();
|
|
559
|
+
});
|
|
560
|
+
program.command("info").description("Print subOrgId and walletAddress from local config").action(async () => {
|
|
561
|
+
await cmdInfo();
|
|
562
|
+
});
|
|
563
|
+
program.command("skill").description(
|
|
564
|
+
"Install the KeeperHub skill file into detected agent directories"
|
|
565
|
+
).addCommand(
|
|
566
|
+
new Command("install").description(
|
|
567
|
+
"Write skill file + register PreToolUse hook in all detected agents"
|
|
568
|
+
).action(async () => {
|
|
569
|
+
const result = await installSkill();
|
|
570
|
+
for (const write of result.skillWrites) {
|
|
571
|
+
process.stdout.write(
|
|
572
|
+
`skill: ${write.agent} -> ${write.path} (${write.status})
|
|
573
|
+
`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
for (const reg of result.hookRegistrations) {
|
|
577
|
+
if (reg.status === "registered") {
|
|
578
|
+
process.stdout.write(
|
|
579
|
+
`hook: ${reg.agent} -> PreToolUse registered
|
|
580
|
+
`
|
|
581
|
+
);
|
|
582
|
+
} else if (reg.status === "notice") {
|
|
583
|
+
process.stderr.write(
|
|
584
|
+
`notice: ${reg.agent} -> ${reg.message ?? ""}
|
|
585
|
+
`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (result.skillWrites.length === 0) {
|
|
590
|
+
process.stderr.write(
|
|
591
|
+
"No supported agent skill directories detected under $HOME. Create ~/.claude/, ~/.cursor/, ~/.cline/, ~/.windsurf/, or ~/.config/opencode/ and re-run.\n"
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
})
|
|
595
|
+
);
|
|
596
|
+
try {
|
|
597
|
+
await program.parseAsync(argv);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
if (err instanceof WalletConfigMissingError) {
|
|
600
|
+
process.stderr.write(`[keeperhub-wallet] ${err.message}
|
|
601
|
+
`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
process.stderr.write(
|
|
605
|
+
`[keeperhub-wallet] ${err.message ?? String(err)}
|
|
606
|
+
`
|
|
607
|
+
);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/safety-config.ts
|
|
613
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
614
|
+
import { homedir as homedir3 } from "os";
|
|
615
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
616
|
+
var DEFAULT_SAFETY_CONFIG = {
|
|
617
|
+
auto_approve_max_usd: 5,
|
|
618
|
+
ask_threshold_usd: 50,
|
|
619
|
+
block_threshold_usd: 100,
|
|
620
|
+
allowlisted_contracts: [
|
|
621
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
622
|
+
// Base USDC
|
|
623
|
+
"0x20c000000000000000000000b9537d11c60e8b50"
|
|
624
|
+
// Tempo USDC.e
|
|
625
|
+
]
|
|
626
|
+
};
|
|
627
|
+
function getSafetyPath() {
|
|
628
|
+
return join4(homedir3(), ".keeperhub", "safety.json");
|
|
629
|
+
}
|
|
630
|
+
async function loadSafetyConfig() {
|
|
631
|
+
const path = getSafetyPath();
|
|
632
|
+
let raw;
|
|
633
|
+
try {
|
|
634
|
+
raw = await readFile3(path, "utf-8");
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (err.code === "ENOENT") {
|
|
637
|
+
await mkdir3(dirname4(path), { recursive: true, mode: 448 });
|
|
638
|
+
await writeFile3(path, JSON.stringify(DEFAULT_SAFETY_CONFIG, null, 2), {
|
|
639
|
+
mode: 420
|
|
640
|
+
});
|
|
641
|
+
await chmod3(path, 420);
|
|
642
|
+
return DEFAULT_SAFETY_CONFIG;
|
|
643
|
+
}
|
|
644
|
+
throw err;
|
|
645
|
+
}
|
|
646
|
+
const parsed = JSON.parse(raw);
|
|
647
|
+
return validateAndMerge(parsed);
|
|
648
|
+
}
|
|
649
|
+
var THRESHOLD_KEYS = [
|
|
650
|
+
"auto_approve_max_usd",
|
|
651
|
+
"ask_threshold_usd",
|
|
652
|
+
"block_threshold_usd"
|
|
653
|
+
];
|
|
654
|
+
function validateAndMerge(partial) {
|
|
655
|
+
const merged = {
|
|
656
|
+
auto_approve_max_usd: partial.auto_approve_max_usd ?? DEFAULT_SAFETY_CONFIG.auto_approve_max_usd,
|
|
657
|
+
ask_threshold_usd: partial.ask_threshold_usd ?? DEFAULT_SAFETY_CONFIG.ask_threshold_usd,
|
|
658
|
+
block_threshold_usd: partial.block_threshold_usd ?? DEFAULT_SAFETY_CONFIG.block_threshold_usd,
|
|
659
|
+
allowlisted_contracts: partial.allowlisted_contracts ?? DEFAULT_SAFETY_CONFIG.allowlisted_contracts
|
|
660
|
+
};
|
|
661
|
+
for (const key of THRESHOLD_KEYS) {
|
|
662
|
+
const v = merged[key];
|
|
663
|
+
if (!(Number.isFinite(v) && v >= 0)) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`safety.json: ${key} must be a non-negative finite number; got ${String(v)}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (merged.ask_threshold_usd < merged.auto_approve_max_usd) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
"safety.json: ask_threshold_usd must be >= auto_approve_max_usd"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
if (merged.block_threshold_usd < merged.ask_threshold_usd) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
"safety.json: block_threshold_usd must be >= ask_threshold_usd"
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (!Array.isArray(merged.allowlisted_contracts)) {
|
|
680
|
+
throw new Error("safety.json: allowlisted_contracts must be an array");
|
|
681
|
+
}
|
|
682
|
+
merged.allowlisted_contracts = merged.allowlisted_contracts.map(
|
|
683
|
+
(a) => a.toLowerCase()
|
|
684
|
+
);
|
|
685
|
+
return merged;
|
|
686
|
+
}
|
|
687
|
+
function getSafetyConfigPath() {
|
|
688
|
+
return getSafetyPath();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/hook.ts
|
|
692
|
+
var DEFAULT_POLL = { intervalMs: 2e3, maxAttempts: 150 };
|
|
693
|
+
var APPROVAL_URL_BASE = "https://app.keeperhub.com/approve/";
|
|
694
|
+
var USDC_DECIMALS2 = 1e6;
|
|
695
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
696
|
+
var MICRO_USDC_RE = /^\d+$/;
|
|
697
|
+
var DEFAULT_TOOL_RE = /keeperhub|wallet|sign/i;
|
|
698
|
+
function defaultToolMatcher(name) {
|
|
699
|
+
return DEFAULT_TOOL_RE.test(name);
|
|
700
|
+
}
|
|
701
|
+
function extractAmountMicroUsdc(input) {
|
|
702
|
+
const ti = input.tool_input ?? {};
|
|
703
|
+
const challenge = ti.paymentChallenge ?? {};
|
|
704
|
+
const directAmount = challenge.amount ?? ti.amount;
|
|
705
|
+
const directUnit = challenge.unit ?? ti.unit;
|
|
706
|
+
if (directAmount === void 0 || directAmount === null) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
if (directUnit !== "usd" && directUnit !== "microUsdc") {
|
|
710
|
+
throw new TypeError(
|
|
711
|
+
`Amount input must be tagged with unit:"usd" or unit:"microUsdc"; got unit=${JSON.stringify(directUnit)}. GUARD-05 refuses to guess - specify explicitly.`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (directUnit === "microUsdc") {
|
|
715
|
+
if (!(typeof directAmount === "string" && MICRO_USDC_RE.test(directAmount))) {
|
|
716
|
+
throw new TypeError(
|
|
717
|
+
`unit:"microUsdc" requires amount as a non-negative integer string; got ${typeof directAmount}`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
return BigInt(directAmount);
|
|
721
|
+
}
|
|
722
|
+
if (!(typeof directAmount === "number" && Number.isFinite(directAmount) && directAmount >= 0)) {
|
|
723
|
+
throw new TypeError(
|
|
724
|
+
`unit:"usd" requires amount as a finite non-negative number; got ${typeof directAmount}`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return BigInt(Math.round(directAmount * USDC_DECIMALS2));
|
|
728
|
+
}
|
|
729
|
+
function extractToAddress(input) {
|
|
730
|
+
const ti = input.tool_input ?? {};
|
|
731
|
+
const challenge = ti.paymentChallenge ?? {};
|
|
732
|
+
const to = ti.to ?? challenge.payTo ?? challenge.to;
|
|
733
|
+
if (typeof to === "string" && ADDRESS_RE.test(to)) {
|
|
734
|
+
return to.toLowerCase();
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
function usdToMicro(usd) {
|
|
739
|
+
return BigInt(Math.round(usd * USDC_DECIMALS2));
|
|
740
|
+
}
|
|
741
|
+
async function createPreToolUseHook(options = {}) {
|
|
742
|
+
const toolMatcher = options.toolNameMatcher ?? defaultToolMatcher;
|
|
743
|
+
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
|
+
const safety = await configLoader();
|
|
755
|
+
return async (raw) => {
|
|
756
|
+
const hookInput = raw ?? {};
|
|
757
|
+
if (!(typeof hookInput.tool_name === "string" && toolMatcher(hookInput.tool_name))) {
|
|
758
|
+
return { decision: "allow" };
|
|
759
|
+
}
|
|
760
|
+
const toAddr = extractToAddress(hookInput);
|
|
761
|
+
const amountMicro = extractAmountMicroUsdc(hookInput);
|
|
762
|
+
if (toAddr && !safety.allowlisted_contracts.includes(toAddr)) {
|
|
763
|
+
return { decision: "deny", reason: "CONTRACT_NOT_ALLOWLISTED" };
|
|
764
|
+
}
|
|
765
|
+
if (amountMicro === null) {
|
|
766
|
+
return { decision: "deny", reason: "AMOUNT_UNDETERMINED" };
|
|
767
|
+
}
|
|
768
|
+
const blockMicro = usdToMicro(safety.block_threshold_usd);
|
|
769
|
+
const askMicro = usdToMicro(safety.ask_threshold_usd);
|
|
770
|
+
const autoMicro = usdToMicro(safety.auto_approve_max_usd);
|
|
771
|
+
if (amountMicro > blockMicro) {
|
|
772
|
+
return { decision: "deny", reason: "BLOCKED_BY_SAFETY_RULE" };
|
|
773
|
+
}
|
|
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
|
+
amountMicroUsdc: amountMicro.toString(),
|
|
779
|
+
toAddress: toAddr ?? "",
|
|
780
|
+
reason: `Agent tool ${hookInput.tool_name}`
|
|
781
|
+
});
|
|
782
|
+
const approvalId = created.approvalRequestId;
|
|
783
|
+
onAskOpen(`${APPROVAL_URL_BASE}${approvalId}`);
|
|
784
|
+
for (let attempt = 0; attempt < poll.maxAttempts; attempt++) {
|
|
785
|
+
await new Promise((r) => setTimeout(r, poll.intervalMs));
|
|
786
|
+
const status = await client.request("GET", `/api/agentic-wallet/approval-request/${approvalId}`);
|
|
787
|
+
if (!("status" in status)) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (status.status === "approved") {
|
|
791
|
+
return { decision: "allow" };
|
|
792
|
+
}
|
|
793
|
+
if (status.status === "rejected") {
|
|
794
|
+
return { decision: "deny", reason: "USER_REJECTED" };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { decision: "deny", reason: "APPROVAL_TIMEOUT" };
|
|
798
|
+
}
|
|
799
|
+
if (amountMicro <= autoMicro) {
|
|
800
|
+
return { decision: "allow" };
|
|
801
|
+
}
|
|
802
|
+
return { decision: "ask" };
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/hook-entrypoint.ts
|
|
807
|
+
async function runHookCli() {
|
|
808
|
+
const hook = await createPreToolUseHook();
|
|
809
|
+
let raw = "";
|
|
810
|
+
for await (const chunk of process.stdin) {
|
|
811
|
+
raw += chunk.toString("utf-8");
|
|
812
|
+
}
|
|
813
|
+
let parsed;
|
|
814
|
+
try {
|
|
815
|
+
parsed = raw.trim().length > 0 ? JSON.parse(raw) : {};
|
|
816
|
+
} catch (err) {
|
|
817
|
+
process.stderr.write(
|
|
818
|
+
`[keeperhub-wallet] hook input is not valid JSON: ${err.message}
|
|
819
|
+
`
|
|
820
|
+
);
|
|
821
|
+
process.exit(2);
|
|
822
|
+
}
|
|
823
|
+
const decision = await hook(parsed);
|
|
824
|
+
const output = {
|
|
825
|
+
hookSpecificOutput: {
|
|
826
|
+
hookEventName: "PreToolUse",
|
|
827
|
+
permissionDecision: decision.decision,
|
|
828
|
+
...decision.reason ? { permissionDecisionReason: decision.reason } : {}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
process.stdout.write(JSON.stringify(output));
|
|
832
|
+
process.exit(decision.decision === "deny" ? 2 : 0);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/mpp-detect.ts
|
|
836
|
+
var MPP_PREFIX = "Payment ";
|
|
837
|
+
function parseMppChallenge(response) {
|
|
838
|
+
const header = response.headers.get("WWW-Authenticate");
|
|
839
|
+
if (!header) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
if (!header.startsWith(MPP_PREFIX)) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
const serialized = header.slice(MPP_PREFIX.length).trim();
|
|
846
|
+
if (serialized.length === 0) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
return { serialized };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/payment-signer.ts
|
|
853
|
+
import { randomBytes } from "crypto";
|
|
854
|
+
|
|
855
|
+
// src/x402-detect.ts
|
|
856
|
+
function isX402Shape(value) {
|
|
857
|
+
if (typeof value !== "object" || value === null) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
const v = value;
|
|
861
|
+
if (v.x402Version !== 2) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
if (!Array.isArray(v.accepts) || v.accepts.length === 0) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
const first = v.accepts[0];
|
|
868
|
+
if (first.scheme !== "exact") {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
async function parseX402Challenge(response) {
|
|
874
|
+
const headerB64 = response.headers.get("PAYMENT-REQUIRED");
|
|
875
|
+
if (headerB64) {
|
|
876
|
+
try {
|
|
877
|
+
const decoded = JSON.parse(
|
|
878
|
+
Buffer.from(headerB64, "base64").toString("utf-8")
|
|
879
|
+
);
|
|
880
|
+
if (isX402Shape(decoded)) {
|
|
881
|
+
return decoded;
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const clone = response.clone();
|
|
888
|
+
const body = await clone.json();
|
|
889
|
+
if (isX402Shape(body)) {
|
|
890
|
+
return body;
|
|
891
|
+
}
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/payment-signer.ts
|
|
898
|
+
var TEMPO_CHAIN_ID = 4217;
|
|
899
|
+
var DEFAULT_APPROVAL_POLL = { intervalMs: 2e3, maxAttempts: 150 };
|
|
900
|
+
var VALID_AFTER_PAST_SLACK_SECONDS = 60;
|
|
901
|
+
var NONCE_BYTES = 32;
|
|
902
|
+
async function sleep(ms) {
|
|
903
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
904
|
+
}
|
|
905
|
+
function createPaymentSigner(opts = {}) {
|
|
906
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
907
|
+
const walletLoader = opts.walletLoader ?? readWalletConfig;
|
|
908
|
+
const clientFactory = opts.clientFactory ?? ((wallet) => new KeeperHubClient(wallet, { fetch: fetchImpl }));
|
|
909
|
+
const pollCfg = opts.approval ?? DEFAULT_APPROVAL_POLL;
|
|
910
|
+
async function signOrPoll(client, body) {
|
|
911
|
+
const result = await client.request(
|
|
912
|
+
"POST",
|
|
913
|
+
"/api/agentic-wallet/sign",
|
|
914
|
+
body
|
|
915
|
+
);
|
|
916
|
+
if ("_status" in result && result._status === 202) {
|
|
917
|
+
const approvalRequestId = result.approvalRequestId;
|
|
918
|
+
for (let attempt = 0; attempt < pollCfg.maxAttempts; attempt++) {
|
|
919
|
+
await sleep(pollCfg.intervalMs);
|
|
920
|
+
const status = await client.request(
|
|
921
|
+
"GET",
|
|
922
|
+
`/api/agentic-wallet/approval-request/${approvalRequestId}`
|
|
923
|
+
);
|
|
924
|
+
if ("status" in status && status.status !== "pending") {
|
|
925
|
+
if (status.status === "rejected") {
|
|
926
|
+
throw new KeeperHubError(
|
|
927
|
+
"APPROVAL_REJECTED",
|
|
928
|
+
"User rejected the operation"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const retry = await client.request(
|
|
932
|
+
"POST",
|
|
933
|
+
"/api/agentic-wallet/sign",
|
|
934
|
+
body
|
|
935
|
+
);
|
|
936
|
+
if ("_status" in retry) {
|
|
937
|
+
throw new KeeperHubError(
|
|
938
|
+
"APPROVAL_LOOP",
|
|
939
|
+
"Sign returned 202 again after approval"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return retry.signature;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
throw new KeeperHubError(
|
|
946
|
+
"APPROVAL_TIMEOUT",
|
|
947
|
+
`No human response within ${pollCfg.intervalMs * pollCfg.maxAttempts}ms`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
return result.signature;
|
|
951
|
+
}
|
|
952
|
+
async function payViaMpp(response, mpp, wallet) {
|
|
953
|
+
const client = clientFactory(wallet);
|
|
954
|
+
const signature = await signOrPoll(client, {
|
|
955
|
+
chain: "tempo",
|
|
956
|
+
paymentChallenge: {
|
|
957
|
+
kind: "mpp",
|
|
958
|
+
serialized: mpp.serialized,
|
|
959
|
+
chainId: TEMPO_CHAIN_ID
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
return fetchImpl(response.url, {
|
|
963
|
+
method: "POST",
|
|
964
|
+
headers: { Authorization: `Payment ${signature}` }
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async function payViaX402(response, x402, wallet) {
|
|
968
|
+
const accept = x402.accepts[0];
|
|
969
|
+
if (!accept) {
|
|
970
|
+
throw new KeeperHubError(
|
|
971
|
+
"X402_EMPTY_ACCEPTS",
|
|
972
|
+
"x402 challenge has no accepts entries"
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
976
|
+
const validAfter = now - VALID_AFTER_PAST_SLACK_SECONDS;
|
|
977
|
+
const validBefore = now + accept.maxTimeoutSeconds;
|
|
978
|
+
const nonce = `0x${randomBytes(NONCE_BYTES).toString("hex")}`;
|
|
979
|
+
const client = clientFactory(wallet);
|
|
980
|
+
const signature = await signOrPoll(client, {
|
|
981
|
+
chain: "base",
|
|
982
|
+
paymentChallenge: {
|
|
983
|
+
kind: "x402",
|
|
984
|
+
payTo: accept.payTo,
|
|
985
|
+
amount: accept.amount,
|
|
986
|
+
validAfter,
|
|
987
|
+
validBefore,
|
|
988
|
+
nonce
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
const paymentSigPayload = {
|
|
992
|
+
payload: {
|
|
993
|
+
authorization: {
|
|
994
|
+
from: wallet.walletAddress,
|
|
995
|
+
to: accept.payTo,
|
|
996
|
+
value: accept.amount,
|
|
997
|
+
validAfter,
|
|
998
|
+
validBefore,
|
|
999
|
+
nonce
|
|
1000
|
+
},
|
|
1001
|
+
signature
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const paymentSigHeader = Buffer.from(
|
|
1005
|
+
JSON.stringify(paymentSigPayload)
|
|
1006
|
+
).toString("base64");
|
|
1007
|
+
const retryUrl = x402.resource.url || response.url;
|
|
1008
|
+
return fetchImpl(retryUrl, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: { "PAYMENT-SIGNATURE": paymentSigHeader }
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
async pay(response) {
|
|
1015
|
+
if (response.status !== 402) {
|
|
1016
|
+
return response;
|
|
1017
|
+
}
|
|
1018
|
+
const x402 = await parseX402Challenge(response);
|
|
1019
|
+
const mpp = parseMppChallenge(response);
|
|
1020
|
+
if (!(x402 || mpp)) {
|
|
1021
|
+
return response;
|
|
1022
|
+
}
|
|
1023
|
+
const wallet = await walletLoader();
|
|
1024
|
+
if (mpp) {
|
|
1025
|
+
return payViaMpp(response, mpp, wallet);
|
|
1026
|
+
}
|
|
1027
|
+
if (x402) {
|
|
1028
|
+
return payViaX402(response, x402, wallet);
|
|
1029
|
+
}
|
|
1030
|
+
return response;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
var paymentSigner = createPaymentSigner();
|
|
1035
|
+
export {
|
|
1036
|
+
BASE_USDC,
|
|
1037
|
+
DEFAULT_SAFETY_CONFIG,
|
|
1038
|
+
KeeperHubClient,
|
|
1039
|
+
KeeperHubError,
|
|
1040
|
+
TEMPO_USDC_E,
|
|
1041
|
+
WalletConfigMissingError,
|
|
1042
|
+
base,
|
|
1043
|
+
buildHmacHeaders,
|
|
1044
|
+
checkBalance,
|
|
1045
|
+
computeSignature,
|
|
1046
|
+
createPaymentSigner,
|
|
1047
|
+
createPreToolUseHook,
|
|
1048
|
+
detectAgents,
|
|
1049
|
+
fund,
|
|
1050
|
+
getSafetyConfigPath,
|
|
1051
|
+
getWalletConfigPath,
|
|
1052
|
+
installSkill,
|
|
1053
|
+
loadSafetyConfig,
|
|
1054
|
+
parseMppChallenge,
|
|
1055
|
+
parseX402Challenge,
|
|
1056
|
+
paymentSigner,
|
|
1057
|
+
readWalletConfig,
|
|
1058
|
+
registerClaudeCodeHook,
|
|
1059
|
+
runCli,
|
|
1060
|
+
runHookCli,
|
|
1061
|
+
tempo,
|
|
1062
|
+
validateAndMerge,
|
|
1063
|
+
writeWalletConfig
|
|
1064
|
+
};
|
|
1065
|
+
//# sourceMappingURL=index.js.map
|