@agentkv/cli 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.
@@ -0,0 +1,720 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/keystore.ts
4
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ import { generateAccountKey, isAccountKeyFormat } from "@agentkv/client";
8
+ import { bytesToHex } from "viem";
9
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
10
+ var POSIX = process.platform !== "win32";
11
+ var KEY_RE = /^0x[0-9a-fA-F]{64}$/;
12
+ function agentkvDir(env = process.env) {
13
+ const override = env.AGENTKV_HOME?.trim();
14
+ return override ? override : join(homedir(), ".agentkv");
15
+ }
16
+ function walletPath(env = process.env) {
17
+ return join(agentkvDir(env), "wallet.json");
18
+ }
19
+ function accountPath(env = process.env) {
20
+ return join(agentkvDir(env), "account.json");
21
+ }
22
+ function ensureDir(env) {
23
+ const dir = agentkvDir(env);
24
+ mkdirSync(dir, { recursive: true, mode: POSIX ? 448 : void 0 });
25
+ if (POSIX) {
26
+ try {
27
+ chmodSync(dir, 448);
28
+ } catch {
29
+ }
30
+ }
31
+ }
32
+ function writeKeystoreFile(file, body) {
33
+ writeFileSync(file, `${JSON.stringify(body, null, 2)}
34
+ `, { mode: 384, flag: "wx" });
35
+ if (POSIX) {
36
+ try {
37
+ chmodSync(file, 384);
38
+ } catch {
39
+ }
40
+ }
41
+ }
42
+ function readKey(file) {
43
+ try {
44
+ const j = JSON.parse(readFileSync(file, "utf8"));
45
+ return typeof j.privateKey === "string" && KEY_RE.test(j.privateKey) ? j.privateKey : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function peekStoredWallet(env = process.env) {
51
+ const file = walletPath(env);
52
+ const key = readKey(file);
53
+ return key ? { address: privateKeyToAccount(key).address, path: file } : null;
54
+ }
55
+ function getOrCreateStoredWallet(env = process.env) {
56
+ const file = walletPath(env);
57
+ const existing = readKey(file);
58
+ if (existing) {
59
+ return {
60
+ privateKey: existing,
61
+ address: privateKeyToAccount(existing).address,
62
+ path: file,
63
+ created: false
64
+ };
65
+ }
66
+ ensureDir(env);
67
+ const privateKey = generatePrivateKey();
68
+ const address = privateKeyToAccount(privateKey).address;
69
+ try {
70
+ writeKeystoreFile(file, { address, privateKey });
71
+ } catch (e) {
72
+ if (e?.code === "EEXIST") {
73
+ const k = readKey(file);
74
+ if (k)
75
+ return {
76
+ privateKey: k,
77
+ address: privateKeyToAccount(k).address,
78
+ path: file,
79
+ created: false
80
+ };
81
+ }
82
+ throw e;
83
+ }
84
+ return { privateKey, address, path: file, created: true };
85
+ }
86
+ var ENC_RE = /^0x[0-9a-fA-F]{64}$/;
87
+ function peekStoredAccount(env = process.env) {
88
+ const file = accountPath(env);
89
+ let raw;
90
+ try {
91
+ raw = readFileSync(file, "utf8");
92
+ } catch (e) {
93
+ if (e?.code === "ENOENT") return null;
94
+ throw e;
95
+ }
96
+ let j;
97
+ try {
98
+ j = JSON.parse(raw);
99
+ } catch {
100
+ throw new Error(`${file} is not valid JSON`);
101
+ }
102
+ if (!isAccountKeyFormat(j.accountKey)) {
103
+ throw new Error(
104
+ `${file} has a missing or malformed accountKey (expected ak_<64 lowercase hex>)`
105
+ );
106
+ }
107
+ if (typeof j.encryptionKey !== "string" || !ENC_RE.test(j.encryptionKey)) {
108
+ throw new Error(
109
+ `${file} has a missing or malformed encryptionKey (expected 0x followed by 64 hex chars)`
110
+ );
111
+ }
112
+ return { accountKey: j.accountKey, encryptionKey: j.encryptionKey, path: file };
113
+ }
114
+ function createStoredAccount(env = process.env) {
115
+ const file = accountPath(env);
116
+ ensureDir(env);
117
+ const accountKey = generateAccountKey();
118
+ const encryptionKey = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
119
+ writeKeystoreFile(file, { accountKey, encryptionKey });
120
+ return { accountKey, encryptionKey, path: file };
121
+ }
122
+
123
+ // src/config.ts
124
+ import { readFileSync as readFileSync2 } from "fs";
125
+ import { join as join2 } from "path";
126
+ import { AgentKV, AgentKVError, isAccountKeyFormat as isAccountKeyFormat2 } from "@agentkv/client";
127
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
128
+
129
+ // src/awal.ts
130
+ import { execFile } from "child_process";
131
+ var AWAL_SPEC = "awal@2.12.0";
132
+ var AK_RE = /^ak_[0-9a-f]{64}$/;
133
+ var AK_REDACT = /ak_[0-9a-f]{64}/g;
134
+ var TIMEOUT_MS = 12e4;
135
+ var defaultExec = (cmd, args, opts) => new Promise((resolve, reject) => {
136
+ execFile(cmd, args, { timeout: opts.timeoutMs, windowsHide: true }, (err, stdout) => {
137
+ if (err) reject(err);
138
+ else resolve({ stdout: String(stdout) });
139
+ });
140
+ });
141
+ function redact(text) {
142
+ return text.replace(AK_REDACT, "ak_\u2026");
143
+ }
144
+ function awalTopoffPayer(exec = defaultExec) {
145
+ return async (req) => {
146
+ if (!AK_RE.test(req.accountKey)) {
147
+ throw new Error("awal top-off: invalid account key format");
148
+ }
149
+ let url;
150
+ try {
151
+ url = new URL(req.depositUrl);
152
+ } catch {
153
+ throw new Error("awal top-off: depositUrl is not a valid URL");
154
+ }
155
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
156
+ throw new Error("awal top-off: depositUrl must be an http(s) URL");
157
+ }
158
+ const pathname = url.pathname.replace(/^\/v1(?=\/)/, "");
159
+ if (pathname !== "/account/deposit") {
160
+ throw new Error("awal top-off: refusing to pay a non-deposit route");
161
+ }
162
+ if (!Number.isInteger(req.maxAmountAtomic) || req.maxAmountAtomic <= 0) {
163
+ throw new Error("awal top-off: maxAmountAtomic must be a positive integer");
164
+ }
165
+ const args = [
166
+ "-y",
167
+ AWAL_SPEC,
168
+ "x402",
169
+ "pay",
170
+ url.toString(),
171
+ "-X",
172
+ "POST",
173
+ "-h",
174
+ JSON.stringify({ Authorization: `Bearer ${req.accountKey}` }),
175
+ "--max-amount",
176
+ String(req.maxAmountAtomic),
177
+ "--json"
178
+ ];
179
+ let stdout;
180
+ try {
181
+ ({ stdout } = await exec("npx", args, { timeoutMs: TIMEOUT_MS }));
182
+ } catch (e) {
183
+ const msg = e instanceof Error ? e.message : String(e);
184
+ throw new Error(`awal payment failed: ${redact(msg)}`);
185
+ }
186
+ let out;
187
+ try {
188
+ const jsonStart = stdout.indexOf("{");
189
+ out = JSON.parse(stdout.slice(jsonStart === -1 ? stdout.length : jsonStart));
190
+ } catch {
191
+ throw new Error("awal top-off: could not confirm settlement (unparseable awal output)");
192
+ }
193
+ if (out.error) {
194
+ throw new Error(`awal top-off failed: ${redact(String(out.error))}`);
195
+ }
196
+ };
197
+ }
198
+
199
+ // src/awalInline.ts
200
+ import { execFile as execFile2 } from "child_process";
201
+ var BEARER_RE = /^Bearer ak_[0-9a-f]{64}$/;
202
+ var AK_REDACT2 = /ak_[0-9a-f]{64}/g;
203
+ var TIMEOUT_MS2 = 12e4;
204
+ var defaultExec2 = (cmd, args, opts) => new Promise((resolve, reject) => {
205
+ execFile2(cmd, args, { timeout: opts.timeoutMs, windowsHide: true }, (err, stdout) => {
206
+ if (err) reject(err);
207
+ else resolve({ stdout: String(stdout) });
208
+ });
209
+ });
210
+ function redact2(text) {
211
+ return text.replace(AK_REDACT2, "ak_\u2026");
212
+ }
213
+ function awalInlinePayer(exec = defaultExec2) {
214
+ return async (req) => {
215
+ const bearer = req.headers.Authorization;
216
+ if (typeof bearer !== "string" || !BEARER_RE.test(bearer)) {
217
+ throw new Error("awal inline pay: missing or malformed Authorization bearer");
218
+ }
219
+ let url;
220
+ try {
221
+ url = new URL(req.url);
222
+ } catch {
223
+ throw new Error("awal inline pay: url is not a valid URL");
224
+ }
225
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
226
+ throw new Error("awal inline pay: url must be an http(s) URL");
227
+ }
228
+ const pathname = url.pathname.replace(/^\/v1(?=\/)/, "");
229
+ if (!pathname.startsWith("/kv/")) {
230
+ throw new Error("awal inline pay: refusing to pay a non-/kv/ route");
231
+ }
232
+ if (!Number.isInteger(req.maxAmountAtomic) || req.maxAmountAtomic <= 0) {
233
+ throw new Error("awal inline pay: maxAmountAtomic must be a positive integer");
234
+ }
235
+ if (req.method !== "POST" && req.method !== "GET") {
236
+ throw new Error("awal inline pay: method must be POST or GET");
237
+ }
238
+ const args = [
239
+ "-y",
240
+ AWAL_SPEC,
241
+ "x402",
242
+ "pay",
243
+ url.toString(),
244
+ "-X",
245
+ req.method,
246
+ ...req.method === "POST" && req.body != null ? ["-d", req.body] : [],
247
+ "-h",
248
+ JSON.stringify(req.headers),
249
+ "--max-amount",
250
+ String(req.maxAmountAtomic),
251
+ "--json"
252
+ ];
253
+ let stdout;
254
+ try {
255
+ ({ stdout } = await exec("npx", args, { timeoutMs: TIMEOUT_MS2 }));
256
+ } catch (e) {
257
+ const msg = e instanceof Error ? e.message : String(e);
258
+ throw new Error(`awal inline pay failed: ${redact2(msg)}`);
259
+ }
260
+ let parsed;
261
+ try {
262
+ const jsonStart = stdout.indexOf("{");
263
+ parsed = JSON.parse(stdout.slice(jsonStart === -1 ? stdout.length : jsonStart));
264
+ } catch {
265
+ throw new Error("awal inline pay: could not parse awal --json output");
266
+ }
267
+ if (typeof parsed !== "object" || parsed === null) {
268
+ throw new Error("awal inline pay: could not parse awal --json output");
269
+ }
270
+ if ("success" in parsed && parsed.success === false) {
271
+ const failure = parsed;
272
+ const message = failure.error?.message ?? failure.error?.code ?? "unknown awal error";
273
+ throw new Error(`awal inline pay failed: ${redact2(message)}`);
274
+ }
275
+ if ("error" in parsed && parsed.error) {
276
+ throw new Error(
277
+ `awal inline pay failed: ${redact2(String(parsed.error))}`
278
+ );
279
+ }
280
+ const ok = parsed;
281
+ if (typeof ok.status !== "number") {
282
+ throw new Error("awal inline pay: unrecognized awal --json output (missing status)");
283
+ }
284
+ return {
285
+ status: ok.status,
286
+ // Re-stringify: awal already parses the body as JSON, but the SDK does its own
287
+ // JSON.parse(body) on whatever this hook returns.
288
+ body: JSON.stringify(ok.data ?? null),
289
+ headers: ok.headers ?? {}
290
+ };
291
+ };
292
+ }
293
+
294
+ // src/config.ts
295
+ var DEFAULT_ENDPOINT = "https://api.agentx402.ai";
296
+ function readConfigFile(env = process.env) {
297
+ try {
298
+ const parsed = JSON.parse(
299
+ readFileSync2(join2(agentkvDir(env), "config.json"), "utf8")
300
+ );
301
+ return parsed && typeof parsed === "object" ? parsed : null;
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+ function resolveConfig(flags, env, readFile = () => null) {
307
+ const file = readFile() ?? {};
308
+ const endpoint = flags.endpoint ?? envStr(env.AGENTKV_ENDPOINT) ?? file.endpoint ?? DEFAULT_ENDPOINT;
309
+ return {
310
+ endpoint,
311
+ network: flags.network ?? envStr(env.AGENTKV_NETWORK) ?? file.network ?? "eip155:8453",
312
+ // Per-operation cap (throws on a single op above this).
313
+ maxSpendUsd: flags.maxSpendUsd ?? numOrThrow(env.AGENTKV_MAX_SPEND_USD, "AGENTKV_MAX_SPEND_USD") ?? file.maxSpendUsd,
314
+ // Cumulative, instance-lifetime cap — env-only, opt-in. Kept SEPARATE from the
315
+ // per-op cap: the MCP server is one long-lived client, so coupling them would turn
316
+ // a per-op ceiling into a lifetime budget that eventually blocks every op.
317
+ maxSessionSpendUsd: numOrThrow(
318
+ env.AGENTKV_MAX_SESSION_SPEND_USD,
319
+ "AGENTKV_MAX_SESSION_SPEND_USD"
320
+ ),
321
+ // secrets: env ONLY — never flags or file
322
+ privateKey: envStr(env.AGENTKV_PRIVATE_KEY),
323
+ encryptionKey: envStr(env.AGENTKV_ENCRYPTION_KEY),
324
+ accountKey: envStr(env.AGENTKV_ACCOUNT_KEY),
325
+ // Onramp: flag > env > file > default, matching the precedence used above. The provider
326
+ // is a string id resolved at use-time (getOnrampProvider) so config never imports the
327
+ // provider classes — adding a provider doesn't touch this file.
328
+ onrampProvider: flags.onrampProvider ?? envStr(env.AGENTKV_ONRAMP_PROVIDER) ?? file.onrampProvider ?? "coinbase",
329
+ // Provider config bag. Currently just Coinbase's appId; new keys go here without
330
+ // changing the command. appId is non-secret (a public CDP project id) so it may come
331
+ // from flag/env/file.
332
+ onrampConfig: {
333
+ appId: flags.onrampAppId ?? envStr(env.AGENTKV_ONRAMP_APP_ID) ?? file.onrampAppId
334
+ },
335
+ // Account-key auto top-off: env-only. Validated at client construction
336
+ // (clientFromConfig), where the auth mode is known.
337
+ topoff: envStr(env.AGENTKV_TOPOFF),
338
+ prepayWatermarkUsd: numOrThrow(env.AGENTKV_PREPAY_WATERMARK, "AGENTKV_PREPAY_WATERMARK"),
339
+ prepayTopoffUsd: numOrThrow(env.AGENTKV_PREPAY_TOPOFF, "AGENTKV_PREPAY_TOPOFF"),
340
+ // Inline pay-per-op transport: env-only, account-key mode only (validated at
341
+ // client construction, same as `topoff`, where the auth mode is known).
342
+ inline: envStr(env.AGENTKV_INLINE)
343
+ };
344
+ }
345
+ function envStr(s) {
346
+ if (s === void 0) return void 0;
347
+ const t = s.trim();
348
+ return t === "" ? void 0 : t;
349
+ }
350
+ function numOrThrow(s, name) {
351
+ const v = envStr(s);
352
+ if (v === void 0) return void 0;
353
+ const n = Number(v);
354
+ if (!Number.isFinite(n) || n < 0) {
355
+ throw new Error(`${name} must be a non-negative number (got ${JSON.stringify(v)})`);
356
+ }
357
+ return n;
358
+ }
359
+ function privateKeySigner(pk) {
360
+ return privateKeyToAccount2(pk);
361
+ }
362
+ function clientFromConfig(cfg, opts) {
363
+ const base = {
364
+ endpoint: cfg.endpoint,
365
+ network: cfg.network,
366
+ maxSpendUsd: cfg.maxSpendUsd,
367
+ maxSessionSpendUsd: cfg.maxSessionSpendUsd
368
+ };
369
+ let stored = null;
370
+ if (!cfg.accountKey && !cfg.privateKey) {
371
+ try {
372
+ stored = peekStoredAccount(opts?.env);
373
+ } catch (e) {
374
+ throw new AgentKVError(
375
+ `account.json is corrupt: ${e instanceof Error ? e.message : String(e)}; fix or remove it (or set AGENTKV_ACCOUNT_KEY / AGENTKV_PRIVATE_KEY explicitly)`,
376
+ "invalid_config",
377
+ 0
378
+ );
379
+ }
380
+ }
381
+ const accountKey = cfg.accountKey ?? (cfg.privateKey ? void 0 : stored?.accountKey);
382
+ if (accountKey) {
383
+ if (!isAccountKeyFormat2(accountKey)) {
384
+ throw new Error(
385
+ "AGENTKV_ACCOUNT_KEY must be of the form ak_<64 lowercase hex> (run 'agentkv account new' to mint one)"
386
+ );
387
+ }
388
+ const encryptionKey = cfg.accountKey ? cfg.encryptionKey : stored?.encryptionKey;
389
+ if (!encryptionKey) {
390
+ throw new Error(
391
+ cfg.accountKey ? "account-key mode (AGENTKV_ACCOUNT_KEY) requires AGENTKV_ENCRYPTION_KEY (the local 32-byte AES key)" : "account.json is missing or has a malformed encryptionKey (the local 32-byte AES key)"
392
+ );
393
+ }
394
+ if (cfg.topoff !== void 0 && cfg.topoff !== "awal") {
395
+ throw new AgentKVError(
396
+ `AGENTKV_TOPOFF: unrecognized value ${JSON.stringify(cfg.topoff)} (only "awal" is supported)`,
397
+ "invalid_config",
398
+ 0
399
+ );
400
+ }
401
+ if (cfg.topoff === void 0 && (cfg.prepayWatermarkUsd !== void 0 || cfg.prepayTopoffUsd !== void 0)) {
402
+ throw new AgentKVError(
403
+ "AGENTKV_PREPAY_WATERMARK/AGENTKV_PREPAY_TOPOFF require AGENTKV_TOPOFF=awal",
404
+ "invalid_config",
405
+ 0
406
+ );
407
+ }
408
+ if (cfg.inline !== void 0 && cfg.inline !== "awal") {
409
+ throw new AgentKVError(
410
+ `AGENTKV_INLINE: unrecognized value ${JSON.stringify(cfg.inline)} (only "awal" is supported)`,
411
+ "invalid_config",
412
+ 0
413
+ );
414
+ }
415
+ const opInlinePayer = cfg.inline === "awal" ? awalInlinePayer() : void 0;
416
+ if (cfg.topoff === "awal") {
417
+ return new AgentKV({
418
+ ...base,
419
+ accountKey,
420
+ encryptionKey,
421
+ prepay: {
422
+ watermark: cfg.prepayWatermarkUsd ?? 0.5,
423
+ topoff: cfg.prepayTopoffUsd ?? 1
424
+ },
425
+ topoffPayer: awalTopoffPayer(),
426
+ ...opInlinePayer ? { opInlinePayer } : {}
427
+ });
428
+ }
429
+ return new AgentKV({
430
+ ...base,
431
+ accountKey,
432
+ encryptionKey,
433
+ ...opInlinePayer ? { opInlinePayer } : {}
434
+ });
435
+ }
436
+ if (cfg.topoff !== void 0 || cfg.prepayWatermarkUsd !== void 0 || cfg.prepayTopoffUsd !== void 0 || cfg.inline !== void 0) {
437
+ throw new AgentKVError(
438
+ "AGENTKV_TOPOFF / AGENTKV_PREPAY_* / AGENTKV_INLINE apply to account-key mode only (wallet mode pays its own top-offs)",
439
+ "invalid_config",
440
+ 0
441
+ );
442
+ }
443
+ let privateKey = cfg.privateKey;
444
+ if (!privateKey) {
445
+ const w = getOrCreateStoredWallet(opts?.env);
446
+ privateKey = w.privateKey;
447
+ if (w.created) {
448
+ opts?.notify?.(
449
+ `created a new wallet ${w.address} (saved to ${w.path}). It is your namespace and holds your funds \u2014 fund it, then back it up. View it any time with: agentkv wallet show`
450
+ );
451
+ }
452
+ }
453
+ return cfg.encryptionKey ? new AgentKV({
454
+ ...base,
455
+ signer: privateKeySigner(privateKey),
456
+ encryptionKey: cfg.encryptionKey
457
+ }) : new AgentKV({ ...base, privateKey });
458
+ }
459
+
460
+ // src/onramp.ts
461
+ function coinbaseNetworkName(network) {
462
+ const n = network.trim().toLowerCase();
463
+ if (n === "base" || n === "eip155:8453" || n === "8453") {
464
+ return "base";
465
+ }
466
+ if (n === "eip155:84532" || n === "84532") {
467
+ throw new Error(
468
+ `Coinbase Onramp supports Base mainnet only; no onramp is available for the configured testnet network (${network}). Fund a testnet account from a testnet faucet + a signing wallet instead.`
469
+ );
470
+ }
471
+ throw new Error(
472
+ `coinbase onramp supports Base only; got network ${JSON.stringify(network)}. AgentKV uses Base (eip155:8453) \u2014 set AGENTKV_NETWORK accordingly.`
473
+ );
474
+ }
475
+ var CoinbaseOnramp = class {
476
+ id = "coinbase";
477
+ name = "Coinbase Onramp";
478
+ buildUrl(opts) {
479
+ const appId = opts.config.appId?.trim();
480
+ if (!appId) {
481
+ throw new Error(
482
+ "coinbase onramp requires a CDP project id \u2014 set AGENTKV_ONRAMP_APP_ID (or --onramp-app-id) to the App/Project ID from https://portal.cdp.coinbase.com (Onramp \u2192 your project)."
483
+ );
484
+ }
485
+ const networkName = coinbaseNetworkName(opts.network);
486
+ const url = new URL("https://pay.coinbase.com/buy/select-asset");
487
+ url.searchParams.set("appId", appId);
488
+ url.searchParams.set("addresses", JSON.stringify({ [opts.address]: [networkName] }));
489
+ url.searchParams.set("defaultNetwork", networkName);
490
+ url.searchParams.set("defaultAsset", "USDC");
491
+ url.searchParams.set("assets", JSON.stringify(["USDC"]));
492
+ if (opts.amountUsd !== void 0 && Number.isFinite(opts.amountUsd) && opts.amountUsd > 0) {
493
+ const cents = Math.round(opts.amountUsd * 100 * (1 + Number.EPSILON));
494
+ if (cents === 0) {
495
+ throw new Error(
496
+ `amount ${opts.amountUsd} is below the $0.01 minimum an onramp can pre-fill; use an amount of at least $0.01.`
497
+ );
498
+ }
499
+ url.searchParams.set("presetFiatAmount", (cents / 100).toString());
500
+ }
501
+ return url.toString();
502
+ }
503
+ };
504
+ var PROVIDERS = {
505
+ coinbase: new CoinbaseOnramp()
506
+ };
507
+ function getOnrampProvider(id) {
508
+ const provider = PROVIDERS[id];
509
+ if (!provider) {
510
+ const known = Object.keys(PROVIDERS).sort().join(", ");
511
+ throw new Error(`unknown onramp provider ${JSON.stringify(id)}; known providers: ${known}`);
512
+ }
513
+ return provider;
514
+ }
515
+
516
+ // src/secrets.ts
517
+ import { spawn } from "child_process";
518
+ import {
519
+ closeSync,
520
+ fchmodSync,
521
+ lstatSync,
522
+ mkdtempSync,
523
+ openSync,
524
+ readFileSync as readFileSync3,
525
+ realpathSync,
526
+ writeSync
527
+ } from "fs";
528
+ import { tmpdir } from "os";
529
+ import { join as join3, resolve as resolvePath, sep } from "path";
530
+ var MAX_SECRET_BYTES = 1024 * 1024;
531
+ var SENSITIVE_ENV = [
532
+ "AGENTKV_PRIVATE_KEY",
533
+ "AGENTKV_ENCRYPTION_KEY",
534
+ "AGENTKV_ACCOUNT_KEY",
535
+ "AGENTKV_PAYER_KEY"
536
+ // funded external-payer key (account fund / --from-key) — holds real USDC
537
+ ];
538
+ var SENSITIVE_ENV_PATTERN = /^AGENTKV_.*(PRIVATE_KEY|PAYER_KEY|ENCRYPTION_KEY|MNEMONIC|SEED_PHRASE)$/i;
539
+ function isSensitiveEnvName(name) {
540
+ return SENSITIVE_ENV.includes(name) || SENSITIVE_ENV_PATTERN.test(name);
541
+ }
542
+ function scrubSensitiveEnv(env = process.env) {
543
+ for (const k of Object.keys(env)) {
544
+ if (isSensitiveEnvName(k)) delete env[k];
545
+ }
546
+ }
547
+ function readEnvSecret(envVar) {
548
+ if (isSensitiveEnvName(envVar)) {
549
+ return {
550
+ ok: false,
551
+ error: `refusing to read protected key material from ${envVar}`,
552
+ code: "forbidden_env"
553
+ };
554
+ }
555
+ const value = process.env[envVar];
556
+ if (value === void 0 || value === "") {
557
+ return { ok: false, error: `env var ${envVar} is unset or empty`, code: "env_unset" };
558
+ }
559
+ return { ok: true, value };
560
+ }
561
+ function readFileSecret(path, opts = {}) {
562
+ const isPseudoFs = (p) => /^(\/proc|\/sys)(\/|$)/.test(p);
563
+ const pseudoFsRefusal = {
564
+ ok: false,
565
+ error: "refusing to read from a pseudo-filesystem path",
566
+ code: "forbidden_path"
567
+ };
568
+ if (isPseudoFs(path)) return pseudoFsRefusal;
569
+ let resolved;
570
+ try {
571
+ resolved = realpathSync(path);
572
+ } catch {
573
+ return { ok: false, error: "could not read file", code: "read_failed" };
574
+ }
575
+ if (isPseudoFs(resolved)) return pseudoFsRefusal;
576
+ const within = (child, parent) => child === parent || child.startsWith(parent.endsWith(sep) ? parent : parent + sep);
577
+ const keystore = agentkvDir(process.env);
578
+ let keystoreReal = resolvePath(keystore);
579
+ try {
580
+ keystoreReal = realpathSync(keystore);
581
+ } catch {
582
+ }
583
+ if (within(resolved, keystoreReal) || within(resolved, resolvePath(keystore))) {
584
+ return {
585
+ ok: false,
586
+ error: "refusing to read from the AgentKV keystore directory",
587
+ code: "forbidden_path"
588
+ };
589
+ }
590
+ let st;
591
+ try {
592
+ st = lstatSync(resolved);
593
+ } catch {
594
+ return { ok: false, error: "could not read file", code: "read_failed" };
595
+ }
596
+ if (!st.isFile()) {
597
+ return { ok: false, error: "not a regular file", code: "not_regular_file" };
598
+ }
599
+ if (st.size > MAX_SECRET_BYTES) {
600
+ return { ok: false, error: "file too large for a secret", code: "file_too_large" };
601
+ }
602
+ let value;
603
+ try {
604
+ value = readFileSync3(resolved, "utf8");
605
+ } catch {
606
+ return { ok: false, error: "could not read file", code: "read_failed" };
607
+ }
608
+ if (opts.trim !== false) value = value.replace(/\r?\n$/, "");
609
+ return { ok: true, value };
610
+ }
611
+ function writeSecretFile(value, dest) {
612
+ const path = dest ?? join3(mkdtempSync(join3(tmpdir(), "agentkv-")), "value");
613
+ const fd = openSync(path, "wx", 384);
614
+ try {
615
+ if (process.platform !== "win32") fchmodSync(fd, 384);
616
+ writeSync(fd, value, null, "utf8");
617
+ } finally {
618
+ closeSync(fd);
619
+ }
620
+ return { path, bytes: Buffer.byteLength(value, "utf8") };
621
+ }
622
+ var HIJACK_ENV = /^(LD_|DYLD_|PYTHON|PERL5?|RUBY(OPT|LIB)|NODE_OPTIONS$|NODE_PATH$|NODE_EXTRA_CA_CERTS$|BASH_ENV$|BASH_FUNC_|ENV$|IFS$|PATH$|CDPATH$|GCONV_PATH$|LOCPATH$|NLSPATH$|MALLOC_|PROMPT_COMMAND$|PS4$|GIT_SSH|GIT_PROXY_COMMAND$|GIT_EXTERNAL_DIFF$|GIT_CONFIG|GIT_SSL_CAINFO$|GIT_SSL_NO_VERIFY$|SSL_CERT|.*CA_BUNDLE$|CLASSPATH$|_?JAVA_OPTIONS$|JDK_JAVA_OPTIONS$|JAVA_TOOL_OPTIONS$|COR(ECLR)?_PROFILER$|DOTNET_STARTUP_HOOKS$|WINEDLLOVERRIDES$|PAGER$|EDITOR$|BROWSER$|.*_PROXY$)/i;
623
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
624
+ function forbiddenEnvKey(keys) {
625
+ return keys.find((k) => !ENV_NAME.test(k) || HIJACK_ENV.test(k)) ?? null;
626
+ }
627
+ function runWithSecret(opts) {
628
+ const cap = opts.capBytes ?? 64 * 1024;
629
+ const timeoutMs = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 12e4;
630
+ return new Promise((resolve, reject) => {
631
+ const bad = forbiddenEnvKey([opts.envVar, ...Object.keys(opts.extraEnv ?? {})]);
632
+ if (bad) {
633
+ reject(new Error(`refusing to set process-hijack env var: ${bad}`));
634
+ return;
635
+ }
636
+ const inherited = { ...process.env };
637
+ for (const k of Object.keys(inherited)) {
638
+ if (isSensitiveEnvName(k)) inherited[k] = void 0;
639
+ }
640
+ const child = spawn(opts.command, opts.args ?? [], {
641
+ cwd: opts.cwd,
642
+ env: { ...inherited, ...opts.extraEnv, [opts.envVar]: opts.secret },
643
+ shell: false,
644
+ timeout: timeoutMs,
645
+ killSignal: "SIGKILL",
646
+ // the built-in timeout sends a SIGTERM a child can ignore — escalate
647
+ stdio: ["ignore", "pipe", "pipe"]
648
+ // no stdin: stdin-reading commands get EOF, don't hang
649
+ });
650
+ const outChunks = [];
651
+ const errChunks = [];
652
+ let outBytes = 0;
653
+ let errBytes = 0;
654
+ let truncated = false;
655
+ let settled = false;
656
+ const finalize = (code) => ({
657
+ exit_code: code,
658
+ stdout: Buffer.concat(outChunks).subarray(0, cap).toString("utf8"),
659
+ stderr: Buffer.concat(errChunks).subarray(0, cap).toString("utf8"),
660
+ truncated
661
+ });
662
+ const done = (cb) => {
663
+ if (settled) return;
664
+ settled = true;
665
+ clearTimeout(hard);
666
+ cb();
667
+ };
668
+ const hard = setTimeout(() => {
669
+ try {
670
+ child.kill("SIGKILL");
671
+ } catch {
672
+ }
673
+ truncated = true;
674
+ done(() => resolve(finalize(null)));
675
+ }, timeoutMs + 5e3);
676
+ child.stdout?.on("data", (d) => {
677
+ if (outBytes >= cap) {
678
+ truncated = true;
679
+ return;
680
+ }
681
+ outChunks.push(d);
682
+ outBytes += d.length;
683
+ if (outBytes > cap) truncated = true;
684
+ });
685
+ child.stderr?.on("data", (d) => {
686
+ if (errBytes >= cap) {
687
+ truncated = true;
688
+ return;
689
+ }
690
+ errChunks.push(d);
691
+ errBytes += d.length;
692
+ if (errBytes > cap) truncated = true;
693
+ });
694
+ child.on("error", (e) => done(() => reject(e)));
695
+ child.on("close", (code) => done(() => resolve(finalize(code))));
696
+ });
697
+ }
698
+
699
+ // src/version.ts
700
+ var VERSION = "0.1.0";
701
+
702
+ export {
703
+ agentkvDir,
704
+ accountPath,
705
+ peekStoredWallet,
706
+ getOrCreateStoredWallet,
707
+ peekStoredAccount,
708
+ createStoredAccount,
709
+ readConfigFile,
710
+ resolveConfig,
711
+ clientFromConfig,
712
+ getOnrampProvider,
713
+ scrubSensitiveEnv,
714
+ readEnvSecret,
715
+ readFileSecret,
716
+ writeSecretFile,
717
+ forbiddenEnvKey,
718
+ runWithSecret,
719
+ VERSION
720
+ };