run402 1.69.5 → 1.69.7

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.
Files changed (44) hide show
  1. package/lib/agent.mjs +37 -11
  2. package/lib/apps.mjs +92 -40
  3. package/lib/argparse.mjs +53 -1
  4. package/lib/billing.mjs +65 -24
  5. package/lib/blob.mjs +87 -49
  6. package/lib/cdn.mjs +21 -8
  7. package/lib/contracts.mjs +112 -33
  8. package/lib/deploy-v2.mjs +159 -62
  9. package/lib/domains.mjs +22 -11
  10. package/lib/functions.mjs +42 -6
  11. package/lib/image.mjs +17 -8
  12. package/lib/message.mjs +4 -1
  13. package/lib/secrets.mjs +29 -12
  14. package/lib/sender-domain.mjs +32 -14
  15. package/lib/sites.mjs +39 -16
  16. package/lib/subdomains.mjs +31 -35
  17. package/lib/tier.mjs +26 -4
  18. package/package.json +1 -1
  19. package/sdk/dist/namespaces/ai.d.ts.map +1 -1
  20. package/sdk/dist/namespaces/ai.js +7 -2
  21. package/sdk/dist/namespaces/ai.js.map +1 -1
  22. package/sdk/dist/namespaces/blobs.d.ts +3 -2
  23. package/sdk/dist/namespaces/blobs.d.ts.map +1 -1
  24. package/sdk/dist/namespaces/blobs.js +38 -7
  25. package/sdk/dist/namespaces/blobs.js.map +1 -1
  26. package/sdk/dist/namespaces/blobs.types.d.ts +5 -6
  27. package/sdk/dist/namespaces/blobs.types.d.ts.map +1 -1
  28. package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
  29. package/sdk/dist/namespaces/contracts.js +17 -4
  30. package/sdk/dist/namespaces/contracts.js.map +1 -1
  31. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  32. package/sdk/dist/namespaces/deploy.js +13 -1
  33. package/sdk/dist/namespaces/deploy.js.map +1 -1
  34. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  35. package/sdk/dist/namespaces/functions.js +92 -1
  36. package/sdk/dist/namespaces/functions.js.map +1 -1
  37. package/sdk/dist/node/files.d.ts +11 -5
  38. package/sdk/dist/node/files.d.ts.map +1 -1
  39. package/sdk/dist/node/files.js +31 -7
  40. package/sdk/dist/node/files.js.map +1 -1
  41. package/sdk/dist/validation.d.ts +2 -0
  42. package/sdk/dist/validation.d.ts.map +1 -1
  43. package/sdk/dist/validation.js +10 -0
  44. package/sdk/dist/validation.js.map +1 -1
package/lib/blob.mjs CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  readFileSync,
25
25
  writeFileSync,
26
26
  mkdirSync,
27
+ chmodSync,
27
28
  existsSync,
28
29
  unlinkSync,
29
30
  readdirSync,
@@ -61,7 +62,7 @@ Options:
61
62
  --json NDJSON progress events (for agent consumption)
62
63
  --prefix <p> Prefix filter (ls only)
63
64
  --limit <n> Max results (ls only; default 100, max 1000)
64
- --ttl <seconds> Signed-URL TTL (sign only; default 3600, max 604800)
65
+ --ttl <seconds> Signed-URL TTL (sign only; default 3600, min 60, max 604800)
65
66
 
66
67
  Examples:
67
68
  run402 blob put ./artifact.tgz --project prj_abc123
@@ -151,7 +152,7 @@ Arguments:
151
152
 
152
153
  Options:
153
154
  --project <id> Project ID (defaults to active project)
154
- --ttl <seconds> Signed-URL TTL (default 3600, max 604800)
155
+ --ttl <seconds> Signed-URL TTL (default 3600, min 60, max 604800)
155
156
 
156
157
  Examples:
157
158
  run402 blob sign reports/2025-q4.pdf --project prj_abc123 --ttl 600
@@ -184,7 +185,9 @@ Examples:
184
185
  `,
185
186
  };
186
187
 
187
- const UPLOAD_STATE_DIR = join(homedir(), ".run402", "uploads");
188
+ function uploadStateDir() {
189
+ return join(homedir(), ".run402", "uploads");
190
+ }
188
191
 
189
192
  function die(msg, exit_code = 1) {
190
193
  fail({ code: "BAD_USAGE", message: msg, exit_code });
@@ -226,7 +229,7 @@ function parseArgs(rawArgs) {
226
229
  else if (a === "--prefix") out.prefix = args[++i];
227
230
  else if (a === "--limit") out.limit = parseIntegerFlag("--limit", args[++i], { min: 1, max: 1000 });
228
231
  else if (a === "--output" || a === "-o") out.output = args[++i];
229
- else if (a === "--ttl") out.ttl = parseIntegerFlag("--ttl", args[++i], { min: 1, max: 604800 });
232
+ else if (a === "--ttl") out.ttl = parseIntegerFlag("--ttl", args[++i], { min: 60, max: 604800 });
230
233
  else if (!a.startsWith("--")) out.positional.push(a);
231
234
  }
232
235
  return out;
@@ -259,30 +262,71 @@ async function sha256File(filePath) {
259
262
  return h.digest("hex");
260
263
  }
261
264
 
265
+ function sha256BufferHexAndBase64(body) {
266
+ const digest = createHash("sha256").update(body).digest();
267
+ return {
268
+ hex: digest.toString("hex"),
269
+ base64: digest.toString("base64"),
270
+ };
271
+ }
272
+
273
+ function checksumHeadersForPresignedUrl(url, checksumBase64) {
274
+ let urlHasChecksum = false;
275
+ try {
276
+ urlHasChecksum = new URL(url).searchParams.has("x-amz-checksum-sha256");
277
+ } catch {
278
+ urlHasChecksum = false;
279
+ }
280
+ return urlHasChecksum ? {} : { "x-amz-checksum-sha256": checksumBase64 };
281
+ }
282
+
262
283
  function loadState(uploadId) {
263
- const path = join(UPLOAD_STATE_DIR, `${uploadId}.json`);
284
+ const path = join(uploadStateDir(), `${uploadId}.json`);
264
285
  if (!existsSync(path)) return null;
265
286
  try { return JSON.parse(readFileSync(path, "utf8")); }
266
287
  catch { return null; }
267
288
  }
268
289
 
269
290
  function saveState(state) {
270
- mkdirSync(UPLOAD_STATE_DIR, { recursive: true });
271
- writeFileSync(join(UPLOAD_STATE_DIR, `${state.upload_id}.json`), JSON.stringify(state, null, 2));
291
+ const dir = uploadStateDir();
292
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
293
+ chmodSync(dir, 0o700);
294
+ const diskState = { ...state };
295
+ delete diskState.parts;
296
+ const path = join(dir, `${state.upload_id}.json`);
297
+ writeFileSync(path, JSON.stringify(diskState, null, 2), { mode: 0o600 });
298
+ chmodSync(path, 0o600);
272
299
  }
273
300
 
274
301
  function removeState(uploadId) {
275
- const path = join(UPLOAD_STATE_DIR, `${uploadId}.json`);
302
+ const path = join(uploadStateDir(), `${uploadId}.json`);
276
303
  if (existsSync(path)) unlinkSync(path);
277
304
  }
278
305
 
279
- function findResumableStateForFile(projectId, localPath, key) {
280
- if (!existsSync(UPLOAD_STATE_DIR)) return null;
281
- for (const f of readdirSync(UPLOAD_STATE_DIR)) {
306
+ function fileFingerprint(stat) {
307
+ return {
308
+ file_size: stat.size,
309
+ file_mtime_ms: stat.mtimeMs,
310
+ };
311
+ }
312
+
313
+ function stateMatchesFile(state, fingerprint) {
314
+ return state.file_size === fingerprint.file_size &&
315
+ typeof state.file_mtime_ms === "number" &&
316
+ state.file_mtime_ms === fingerprint.file_mtime_ms;
317
+ }
318
+
319
+ function findResumableStateForFile(projectId, localPath, key, fingerprint) {
320
+ const dir = uploadStateDir();
321
+ if (!existsSync(dir)) return null;
322
+ for (const f of readdirSync(dir)) {
282
323
  if (!f.endsWith(".json")) continue;
283
324
  try {
284
- const s = JSON.parse(readFileSync(join(UPLOAD_STATE_DIR, f), "utf8"));
285
- if (s.project_id === projectId && s.local_path === localPath && s.key === key) return s;
325
+ const s = JSON.parse(readFileSync(join(dir, f), "utf8"));
326
+ if (s.project_id === projectId && s.local_path === localPath && s.key === key) {
327
+ if (stateMatchesFile(s, fingerprint)) return s;
328
+ removeState(s.upload_id);
329
+ }
286
330
  } catch { /* ignore */ }
287
331
  }
288
332
  return null;
@@ -295,16 +339,15 @@ function findResumableStateForFile(projectId, localPath, key) {
295
339
  async function putOne(projectId, filePath, opts) {
296
340
  const stat = statSync(filePath);
297
341
  const size = stat.size;
342
+ const fingerprint = fileFingerprint(stat);
298
343
  const destKey = computeDestKey(filePath, opts.key);
299
344
  const absLocal = resolvePath(filePath);
300
345
 
301
- // Compute sha256 for immutable uploads up front; otherwise lazy.
302
- const needSha = opts.immutable;
303
- const sha256 = needSha ? await sha256File(filePath) : undefined;
346
+ const sha256 = await sha256File(filePath);
304
347
 
305
348
  // Attempt to resume
306
349
  let state = opts.resume
307
- ? findResumableStateForFile(projectId, absLocal, destKey)
350
+ ? findResumableStateForFile(projectId, absLocal, destKey, fingerprint)
308
351
  : null;
309
352
  let initRes;
310
353
  if (state) {
@@ -315,7 +358,7 @@ async function putOne(projectId, filePath, opts) {
315
358
  initRes = {
316
359
  upload_id: state.upload_id,
317
360
  mode: poll.mode ?? state.mode,
318
- parts: poll.parts ?? state.parts,
361
+ parts: poll.parts ?? state.parts ?? [],
319
362
  part_count: poll.part_count ?? state.part_count,
320
363
  part_size_bytes: poll.part_size_bytes ?? state.part_size_bytes,
321
364
  };
@@ -345,6 +388,7 @@ async function putOne(projectId, filePath, opts) {
345
388
  parts: initRes.parts,
346
389
  parts_done: {},
347
390
  sha256,
391
+ ...fingerprint,
348
392
  started_at: new Date().toISOString(),
349
393
  };
350
394
  if (opts.resume) saveState(state);
@@ -360,22 +404,18 @@ async function putOne(projectId, filePath, opts) {
360
404
  etags[parseInt(pn, 10) - 1] = typeof pd === "string" ? { etag: pd, sha256: undefined } : pd;
361
405
  }
362
406
 
363
- // Presigned URLs are signed WITHOUT ChecksumAlgorithm (see gateway
364
- // s3-presign.ts). The client-asserted sha256 declared at init is the
365
- // integrity attestation — no x-amz-checksum-sha256 header on PUTs, and
366
- // the gateway trusts the declared value at complete when S3 has none.
367
407
  const todo = initRes.parts.filter((p) => !(state.parts_done || {})[String(p.part_number)]);
368
408
  await withConcurrency(todo, opts.concurrency, async (part) => {
369
- const { etag } = await putPart(filePath, part);
370
- etags[part.part_number - 1] = { etag };
371
- state.parts_done[String(part.part_number)] = { etag };
409
+ const { etag, sha256: partSha256 } = await putPart(filePath, part);
410
+ etags[part.part_number - 1] = { etag, sha256: partSha256 };
411
+ state.parts_done[String(part.part_number)] = { etag, sha256: partSha256 };
372
412
  if (opts.resume) saveState(state);
373
- log(opts, { event: "part", upload_id: state.upload_id, part_number: part.part_number, etag });
413
+ log(opts, { event: "part", upload_id: state.upload_id, part_number: part.part_number, etag, sha256: partSha256 });
374
414
  });
375
415
 
376
416
  // Complete
377
417
  const body = initRes.mode === "multipart"
378
- ? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag })) }
418
+ ? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag, sha256: e.sha256 })) }
379
419
  : {};
380
420
  const result = await getSdk().blobs.completeUploadSession(projectId, state.upload_id, body, {
381
421
  contentType: opts.contentType ?? guessContentType(destKey),
@@ -399,37 +439,31 @@ async function putPart(filePath, part) {
399
439
  const chunks = [];
400
440
  for await (const c of stream) chunks.push(c);
401
441
  const body = Buffer.concat(chunks);
442
+ const checksum = sha256BufferHexAndBase64(body);
402
443
 
403
- const res = await fetch(part.url, { method: "PUT", body });
444
+ const res = await fetch(part.url, {
445
+ method: "PUT",
446
+ headers: checksumHeadersForPresignedUrl(part.url, checksum.base64),
447
+ body,
448
+ });
404
449
  if (!res.ok) {
405
450
  const errBody = await res.text().catch(() => "");
406
451
  throw new Error(`Part ${part.part_number} PUT failed: ${res.status} ${res.statusText}${errBody ? " — " + errBody.slice(0, 200) : ""}`);
407
452
  }
408
453
  const etag = res.headers.get("etag") ?? "";
409
- return { etag };
454
+ return { etag, sha256: checksum.hex };
410
455
  }
411
456
 
412
457
  async function withConcurrency(items, limit, worker) {
413
- const running = [];
414
- for (const item of items) {
415
- const p = Promise.resolve().then(() => worker(item));
416
- running.push(p);
417
- if (running.length >= limit) {
418
- await Promise.race(running.map((r) => r.catch(() => {})));
419
- for (let i = running.length - 1; i >= 0; i--) {
420
- if (isSettled(running[i])) running.splice(i, 1);
421
- }
458
+ let index = 0;
459
+ const workerCount = Math.min(limit, items.length);
460
+ async function runWorker() {
461
+ while (index < items.length) {
462
+ const item = items[index++];
463
+ await worker(item);
422
464
  }
423
465
  }
424
- await Promise.all(running);
425
- }
426
-
427
- function isSettled(p) {
428
- const marker = {};
429
- return Promise.race([p, marker]).then(
430
- (v) => v !== marker,
431
- () => true,
432
- );
466
+ await Promise.all(Array.from({ length: workerCount }, runWorker));
433
467
  }
434
468
 
435
469
  async function put(projectId, argv) {
@@ -438,8 +472,8 @@ async function put(projectId, argv) {
438
472
  const resolvedId = resolveProjectId(opts.project);
439
473
 
440
474
  if (opts.positional.length === 0) die("At least one file path is required");
441
- if (opts.immutable && opts.positional.length > 1 && opts.key && !opts.key.endsWith("/")) {
442
- die("--key with --immutable across multiple files requires a directory prefix (ending with /)");
475
+ if (opts.positional.length > 1 && opts.key && !opts.key.endsWith("/")) {
476
+ die("--key across multiple files requires a directory prefix (ending with /)");
443
477
  }
444
478
 
445
479
  const results = [];
@@ -464,6 +498,7 @@ async function get(projectId, argv) {
464
498
  opts.project = opts.project || projectId;
465
499
  const resolvedId = resolveProjectId(opts.project);
466
500
  if (opts.positional.length === 0) die("Key required");
501
+ if (opts.positional.length > 1) die("blob get expects exactly one key");
467
502
  if (!opts.output) die("--output <file> required");
468
503
  const key = opts.positional[0];
469
504
 
@@ -510,6 +545,7 @@ async function rm(projectId, argv) {
510
545
  opts.project = opts.project || projectId;
511
546
  const resolvedId = resolveProjectId(opts.project);
512
547
  if (opts.positional.length === 0) die("Key required");
548
+ if (opts.positional.length > 1) die("blob rm expects exactly one key");
513
549
  const key = opts.positional[0];
514
550
 
515
551
  try {
@@ -529,6 +565,7 @@ async function diagnose(projectId, argv) {
529
565
  opts.project = opts.project || projectId;
530
566
  const resolvedId = resolveProjectId(opts.project);
531
567
  if (opts.positional.length === 0) die("URL required");
568
+ if (opts.positional.length > 1) die("blob diagnose expects exactly one URL");
532
569
  const url = opts.positional[0];
533
570
 
534
571
  try {
@@ -556,6 +593,7 @@ async function sign(projectId, argv) {
556
593
  opts.project = opts.project || projectId;
557
594
  const resolvedId = resolveProjectId(opts.project);
558
595
  if (opts.positional.length === 0) die("Key required");
596
+ if (opts.positional.length > 1) die("blob sign expects exactly one key");
559
597
  const key = opts.positional[0];
560
598
 
561
599
  try {
package/lib/cdn.mjs CHANGED
@@ -16,6 +16,7 @@
16
16
  import { resolveProjectId } from "./config.mjs";
17
17
  import { getSdk } from "./sdk.mjs";
18
18
  import { reportSdkError, fail } from "./sdk-errors.mjs";
19
+ import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
19
20
 
20
21
  const HELP = `run402 cdn — CloudFront CDN diagnostics for public blob URLs
21
22
 
@@ -73,14 +74,19 @@ function die(msg, exit_code = 1) {
73
74
  }
74
75
 
75
76
  function parseArgs(args) {
76
- const opts = { positional: [] };
77
- for (let i = 0; i < args.length; i++) {
78
- const a = args[i];
79
- if (a === "--sha") opts.sha = args[++i];
80
- else if (a === "--timeout") opts.timeout = Number(args[++i]);
81
- else if (a === "--project") opts.project = args[++i];
82
- else if (a.startsWith("--")) die(`Unknown option: ${a}`);
83
- else opts.positional.push(a);
77
+ const normalized = normalizeArgv(args);
78
+ const valueFlags = ["--sha", "--timeout", "--project"];
79
+ assertKnownFlags(normalized, [...valueFlags, "--help", "-h"], valueFlags);
80
+ const opts = {
81
+ positional: positionalArgs(normalized, valueFlags),
82
+ sha: flagValue(normalized, "--sha"),
83
+ timeout: normalized.includes("--timeout")
84
+ ? parseIntegerFlag("--timeout", flagValue(normalized, "--timeout"), { min: 1 })
85
+ : undefined,
86
+ project: flagValue(normalized, "--project"),
87
+ };
88
+ if (opts.positional.length > 1) {
89
+ die(`Unexpected argument for cdn wait-fresh: ${opts.positional[1]}`);
84
90
  }
85
91
  return opts;
86
92
  }
@@ -92,6 +98,13 @@ async function waitFresh(projectId, argv) {
92
98
  if (opts.positional.length === 0) die("URL required");
93
99
  const url = opts.positional[0];
94
100
  if (!opts.sha) die("--sha is required");
101
+ if (!/^[a-fA-F0-9]{64}$/.test(opts.sha)) {
102
+ fail({
103
+ code: "BAD_FLAG",
104
+ message: "--sha must be a 64-character hex SHA-256 digest",
105
+ details: { flag: "--sha", value: opts.sha },
106
+ });
107
+ }
95
108
 
96
109
  const timeoutMs = (opts.timeout ?? 60) * 1000;
97
110
  try {
package/lib/contracts.mjs CHANGED
@@ -1,5 +1,13 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
2
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
3
+ import {
4
+ assertAllowedValue,
5
+ assertKnownFlags,
6
+ flagValue,
7
+ normalizeArgv,
8
+ positionalArgs,
9
+ validateEvmAddress,
10
+ } from "./argparse.mjs";
3
11
 
4
12
  const HELP = `run402 contracts — KMS-backed Ethereum wallets for smart-contract calls
5
13
 
@@ -126,12 +134,6 @@ Examples:
126
134
  `,
127
135
  };
128
136
 
129
- function parseFlag(args, flag) {
130
- for (let i = 0; i < args.length; i++) {
131
- if (args[i] === flag && args[i + 1]) return args[i + 1];
132
- }
133
- return null;
134
- }
135
137
  function hasFlag(args, flag) {
136
138
  return args.includes(flag);
137
139
  }
@@ -146,21 +148,30 @@ function validateWeiFlag(flag, value) {
146
148
  }
147
149
 
148
150
  async function provisionWallet(projectId, args) {
149
- const chain = parseFlag(args, "--chain");
151
+ const parsedArgs = normalizeArgv(args);
152
+ const valueFlags = ["--chain", "--recovery"];
153
+ assertKnownFlags(parsedArgs, [...valueFlags, "--yes", "--help", "-h"], valueFlags);
154
+ const extra = positionalArgs(parsedArgs, valueFlags);
155
+ if (extra.length > 0) {
156
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts provision-wallet: ${extra[0]}` });
157
+ }
158
+ const chain = flagValue(parsedArgs, "--chain");
150
159
  if (!chain) {
151
160
  fail({
152
161
  code: "BAD_USAGE",
153
162
  message: "Missing --chain (base-mainnet or base-sepolia)",
154
163
  });
155
164
  }
156
- const recovery = parseFlag(args, "--recovery");
165
+ assertAllowedValue(chain, ["base-mainnet", "base-sepolia"], "--chain");
166
+ const recovery = flagValue(parsedArgs, "--recovery");
167
+ if (recovery) validateEvmAddress(recovery, "--recovery");
157
168
  // Soft default of one wallet — confirm if project already has one.
158
169
  let activeWallets = null;
159
170
  try {
160
171
  const list = await getSdk().contracts.listWallets(projectId);
161
172
  activeWallets = (list.wallets || []).filter((w) => w.status === "active").length;
162
173
  } catch { /* best-effort */ }
163
- if (activeWallets !== null && activeWallets >= 1 && !hasFlag(args, "--yes")) {
174
+ if (activeWallets !== null && activeWallets >= 1 && !hasFlag(parsedArgs, "--yes")) {
164
175
  fail({
165
176
  code: "CONFIRMATION_REQUIRED",
166
177
  message: `This project already has ${activeWallets} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`,
@@ -179,7 +190,13 @@ async function provisionWallet(projectId, args) {
179
190
  }
180
191
  }
181
192
 
182
- async function getWallet(projectId, walletId) {
193
+ async function getWallet(projectId, walletId, args = []) {
194
+ const parsedArgs = normalizeArgv(args);
195
+ assertKnownFlags(parsedArgs, ["--help", "-h"]);
196
+ const extra = positionalArgs(parsedArgs);
197
+ if (extra.length > 0) {
198
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts get-wallet: ${extra[0]}` });
199
+ }
183
200
  try {
184
201
  const data = await getSdk().contracts.getWallet(projectId, walletId);
185
202
  console.log(JSON.stringify(data, null, 2));
@@ -188,7 +205,13 @@ async function getWallet(projectId, walletId) {
188
205
  }
189
206
  }
190
207
 
191
- async function listWallets(projectId) {
208
+ async function listWallets(projectId, args = []) {
209
+ const parsedArgs = normalizeArgv(args);
210
+ assertKnownFlags(parsedArgs, ["--help", "-h"]);
211
+ const extra = positionalArgs(parsedArgs);
212
+ if (extra.length > 0) {
213
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts list-wallets: ${extra[0]}` });
214
+ }
192
215
  try {
193
216
  const data = await getSdk().contracts.listWallets(projectId);
194
217
  console.log(JSON.stringify(data, null, 2));
@@ -198,14 +221,25 @@ async function listWallets(projectId) {
198
221
  }
199
222
 
200
223
  async function setRecovery(projectId, walletId, args) {
201
- const clear = hasFlag(args, "--clear");
202
- const address = parseFlag(args, "--address");
224
+ const parsedArgs = normalizeArgv(args);
225
+ const valueFlags = ["--address"];
226
+ assertKnownFlags(parsedArgs, [...valueFlags, "--clear", "--help", "-h"], valueFlags);
227
+ const extra = positionalArgs(parsedArgs, valueFlags);
228
+ if (extra.length > 0) {
229
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts set-recovery: ${extra[0]}` });
230
+ }
231
+ const clear = hasFlag(parsedArgs, "--clear");
232
+ const address = flagValue(parsedArgs, "--address");
233
+ if (clear && address) {
234
+ fail({ code: "BAD_USAGE", message: "Provide either --address or --clear, not both." });
235
+ }
203
236
  if (!clear && !address) {
204
237
  fail({
205
238
  code: "BAD_USAGE",
206
239
  message: "Provide --address 0x... or --clear",
207
240
  });
208
241
  }
242
+ if (address) validateEvmAddress(address, "--address");
209
243
  try {
210
244
  await getSdk().contracts.setRecovery(projectId, walletId, clear ? null : address);
211
245
  console.log(JSON.stringify({ status: "ok", wallet_id: walletId, recovery_address: clear ? null : address }));
@@ -215,7 +249,14 @@ async function setRecovery(projectId, walletId, args) {
215
249
  }
216
250
 
217
251
  async function setAlert(projectId, walletId, args) {
218
- const threshold = parseFlag(args, "--threshold-wei");
252
+ const parsedArgs = normalizeArgv(args);
253
+ const valueFlags = ["--threshold-wei"];
254
+ assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
255
+ const extra = positionalArgs(parsedArgs, valueFlags);
256
+ if (extra.length > 0) {
257
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts set-alert: ${extra[0]}` });
258
+ }
259
+ const threshold = flagValue(parsedArgs, "--threshold-wei");
219
260
  if (!threshold) {
220
261
  fail({ code: "BAD_USAGE", message: "Missing --threshold-wei <n>" });
221
262
  }
@@ -229,13 +270,20 @@ async function setAlert(projectId, walletId, args) {
229
270
  }
230
271
 
231
272
  async function call(projectId, walletId, args) {
232
- const to = parseFlag(args, "--to");
233
- const abi = parseFlag(args, "--abi");
234
- const fn = parseFlag(args, "--fn");
235
- const argsJson = parseFlag(args, "--args");
236
- const value = parseFlag(args, "--value-wei");
237
- const chain = parseFlag(args, "--chain") || "base-mainnet";
238
- const idempotency = parseFlag(args, "--idempotency-key");
273
+ const parsedArgs = normalizeArgv(args);
274
+ const valueFlags = ["--to", "--abi", "--fn", "--args", "--value-wei", "--chain", "--idempotency-key"];
275
+ assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
276
+ const extra = positionalArgs(parsedArgs, valueFlags);
277
+ if (extra.length > 0) {
278
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts call: ${extra[0]}` });
279
+ }
280
+ const to = flagValue(parsedArgs, "--to");
281
+ const abi = flagValue(parsedArgs, "--abi");
282
+ const fn = flagValue(parsedArgs, "--fn");
283
+ const argsJson = flagValue(parsedArgs, "--args");
284
+ const value = flagValue(parsedArgs, "--value-wei");
285
+ const chain = flagValue(parsedArgs, "--chain") || "base-mainnet";
286
+ const idempotency = flagValue(parsedArgs, "--idempotency-key");
239
287
  if (!to || !abi || !fn || !argsJson) {
240
288
  fail({
241
289
  code: "BAD_USAGE",
@@ -243,9 +291,11 @@ async function call(projectId, walletId, args) {
243
291
  hint: "Cost: chain gas + $0.000005 KMS sign fee.",
244
292
  });
245
293
  }
294
+ assertAllowedValue(chain, ["base-mainnet", "base-sepolia"], "--chain");
246
295
  if (value !== null) validateWeiFlag("--value-wei", value);
247
296
  const abiFragment = parseFlagJson("--abi", abi);
248
297
  const callArgs = parseFlagJson("--args", argsJson);
298
+ validateEvmAddress(to, "--to");
249
299
  try {
250
300
  const data = await getSdk().contracts.call(projectId, {
251
301
  walletId,
@@ -264,19 +314,28 @@ async function call(projectId, walletId, args) {
264
314
  }
265
315
 
266
316
  async function read(args) {
267
- const chain = parseFlag(args, "--chain");
268
- const to = parseFlag(args, "--to");
269
- const abi = parseFlag(args, "--abi");
270
- const fn = parseFlag(args, "--fn");
271
- const argsJson = parseFlag(args, "--args");
317
+ const parsedArgs = normalizeArgv(args);
318
+ const valueFlags = ["--chain", "--to", "--abi", "--fn", "--args"];
319
+ assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
320
+ const extra = positionalArgs(parsedArgs, valueFlags);
321
+ if (extra.length > 0) {
322
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts read: ${extra[0]}` });
323
+ }
324
+ const chain = flagValue(parsedArgs, "--chain");
325
+ const to = flagValue(parsedArgs, "--to");
326
+ const abi = flagValue(parsedArgs, "--abi");
327
+ const fn = flagValue(parsedArgs, "--fn");
328
+ const argsJson = flagValue(parsedArgs, "--args");
272
329
  if (!chain || !to || !abi || !fn || !argsJson) {
273
330
  fail({
274
331
  code: "BAD_USAGE",
275
332
  message: "Required flags: --chain, --to, --abi, --fn, --args",
276
333
  });
277
334
  }
335
+ assertAllowedValue(chain, ["base-mainnet", "base-sepolia"], "--chain");
278
336
  const abiFragment = parseFlagJson("--abi", abi);
279
337
  const callArgs = parseFlagJson("--args", argsJson);
338
+ validateEvmAddress(to, "--to");
280
339
  try {
281
340
  const data = await getSdk().contracts.read({
282
341
  chain,
@@ -291,7 +350,13 @@ async function read(args) {
291
350
  }
292
351
  }
293
352
 
294
- async function status(projectId, callId) {
353
+ async function status(projectId, callId, args = []) {
354
+ const parsedArgs = normalizeArgv(args);
355
+ assertKnownFlags(parsedArgs, ["--help", "-h"]);
356
+ const extra = positionalArgs(parsedArgs);
357
+ if (extra.length > 0) {
358
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts status: ${extra[0]}` });
359
+ }
295
360
  try {
296
361
  const data = await getSdk().contracts.callStatus(projectId, callId);
297
362
  console.log(JSON.stringify(data, null, 2));
@@ -301,14 +366,22 @@ async function status(projectId, callId) {
301
366
  }
302
367
 
303
368
  async function drain(projectId, walletId, args) {
304
- const to = parseFlag(args, "--to");
305
- if (!to || !hasFlag(args, "--confirm")) {
369
+ const parsedArgs = normalizeArgv(args);
370
+ const valueFlags = ["--to"];
371
+ assertKnownFlags(parsedArgs, [...valueFlags, "--confirm", "--help", "-h"], valueFlags);
372
+ const extra = positionalArgs(parsedArgs, valueFlags);
373
+ if (extra.length > 0) {
374
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts drain: ${extra[0]}` });
375
+ }
376
+ const to = flagValue(parsedArgs, "--to");
377
+ if (!to || !hasFlag(parsedArgs, "--confirm")) {
306
378
  fail({
307
379
  code: "BAD_USAGE",
308
380
  message: "Required: --to 0x... and --confirm.",
309
381
  hint: "Cost: chain gas + $0.000005 KMS sign fee.",
310
382
  });
311
383
  }
384
+ validateEvmAddress(to, "--to");
312
385
  try {
313
386
  const data = await getSdk().contracts.drain(projectId, walletId, to);
314
387
  console.log(JSON.stringify(data, null, 2));
@@ -318,7 +391,13 @@ async function drain(projectId, walletId, args) {
318
391
  }
319
392
 
320
393
  async function deleteWallet(projectId, walletId, args) {
321
- if (!hasFlag(args, "--confirm")) {
394
+ const parsedArgs = normalizeArgv(args);
395
+ assertKnownFlags(parsedArgs, ["--confirm", "--help", "-h"]);
396
+ const extra = positionalArgs(parsedArgs);
397
+ if (extra.length > 0) {
398
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts delete: ${extra[0]}` });
399
+ }
400
+ if (!hasFlag(parsedArgs, "--confirm")) {
322
401
  fail({ code: "BAD_USAGE", message: "Required: --confirm" });
323
402
  }
324
403
  try {
@@ -334,13 +413,13 @@ export async function run(sub, args) {
334
413
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
335
414
  switch (sub) {
336
415
  case "provision-wallet": await provisionWallet(args[0], args.slice(1)); break;
337
- case "get-wallet": await getWallet(args[0], args[1]); break;
338
- case "list-wallets": await listWallets(args[0]); break;
416
+ case "get-wallet": await getWallet(args[0], args[1], args.slice(2)); break;
417
+ case "list-wallets": await listWallets(args[0], args.slice(1)); break;
339
418
  case "set-recovery": await setRecovery(args[0], args[1], args.slice(2)); break;
340
419
  case "set-alert": await setAlert(args[0], args[1], args.slice(2)); break;
341
420
  case "call": await call(args[0], args[1], args.slice(2)); break;
342
421
  case "read": await read(args); break;
343
- case "status": await status(args[0], args[1]); break;
422
+ case "status": await status(args[0], args[1], args.slice(2)); break;
344
423
  case "drain": await drain(args[0], args[1], args.slice(2)); break;
345
424
  case "delete": await deleteWallet(args[0], args[1], args.slice(2)); break;
346
425
  default: