run402 1.54.1 → 1.54.3

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 (71) hide show
  1. package/cli.mjs +18 -0
  2. package/core-dist/allowance-auth.js +5 -0
  3. package/core-dist/allowance.js +49 -1
  4. package/core-dist/config.js +35 -2
  5. package/core-dist/wallet-auth.js +62 -0
  6. package/core-dist/wallet.js +25 -0
  7. package/lib/agent.mjs +29 -1
  8. package/lib/ai.mjs +119 -43
  9. package/lib/apps.mjs +43 -9
  10. package/lib/argparse.mjs +234 -0
  11. package/lib/auth.mjs +17 -4
  12. package/lib/billing.mjs +35 -0
  13. package/lib/blob.mjs +55 -22
  14. package/lib/config.mjs +20 -1
  15. package/lib/contracts.mjs +41 -0
  16. package/lib/deploy.mjs +125 -58
  17. package/lib/domains.mjs +79 -5
  18. package/lib/email.mjs +34 -0
  19. package/lib/functions.mjs +104 -30
  20. package/lib/image.mjs +33 -1
  21. package/lib/message.mjs +50 -3
  22. package/lib/projects.mjs +75 -36
  23. package/lib/sdk-errors.mjs +2 -1
  24. package/lib/secrets.mjs +33 -6
  25. package/lib/sender-domain.mjs +78 -1
  26. package/lib/service.mjs +30 -1
  27. package/lib/status.mjs +4 -1
  28. package/lib/subdomains.mjs +31 -2
  29. package/lib/tier.mjs +41 -1
  30. package/lib/webhooks.mjs +10 -0
  31. package/package.json +1 -1
  32. package/sdk/core-dist/allowance-auth.js +5 -0
  33. package/sdk/core-dist/allowance.js +49 -1
  34. package/sdk/core-dist/config.js +35 -2
  35. package/sdk/core-dist/wallet-auth.js +62 -0
  36. package/sdk/core-dist/wallet.js +25 -0
  37. package/sdk/dist/index.d.ts +1 -0
  38. package/sdk/dist/index.d.ts.map +1 -1
  39. package/sdk/dist/index.js +5 -0
  40. package/sdk/dist/index.js.map +1 -1
  41. package/sdk/dist/namespaces/auth.d.ts +7 -0
  42. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  43. package/sdk/dist/namespaces/auth.js +24 -0
  44. package/sdk/dist/namespaces/auth.js.map +1 -1
  45. package/sdk/dist/namespaces/billing.d.ts +3 -0
  46. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/billing.js +6 -0
  48. package/sdk/dist/namespaces/billing.js.map +1 -1
  49. package/sdk/dist/namespaces/contracts.d.ts +3 -0
  50. package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
  51. package/sdk/dist/namespaces/contracts.js +6 -0
  52. package/sdk/dist/namespaces/contracts.js.map +1 -1
  53. package/sdk/dist/namespaces/email.d.ts +4 -0
  54. package/sdk/dist/namespaces/email.d.ts.map +1 -1
  55. package/sdk/dist/namespaces/email.js +8 -0
  56. package/sdk/dist/namespaces/email.js.map +1 -1
  57. package/sdk/dist/namespaces/projects.d.ts +14 -0
  58. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  59. package/sdk/dist/namespaces/projects.js +72 -0
  60. package/sdk/dist/namespaces/projects.js.map +1 -1
  61. package/sdk/dist/namespaces/sender-domain.d.ts +2 -0
  62. package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
  63. package/sdk/dist/namespaces/sender-domain.js +4 -0
  64. package/sdk/dist/namespaces/sender-domain.js.map +1 -1
  65. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  66. package/sdk/dist/node/paid-fetch.js +12 -1
  67. package/sdk/dist/node/paid-fetch.js.map +1 -1
  68. package/sdk/dist/scoped.d.ts +8 -1
  69. package/sdk/dist/scoped.d.ts.map +1 -1
  70. package/sdk/dist/scoped.js +21 -0
  71. package/sdk/dist/scoped.js.map +1 -1
@@ -0,0 +1,234 @@
1
+ import { fail } from "./sdk-errors.mjs";
2
+ import { resolveProjectId } from "./config.mjs";
3
+
4
+ export function normalizeArgv(argv = []) {
5
+ const out = [];
6
+ for (const arg of argv ?? []) {
7
+ if (typeof arg === "string" && arg.startsWith("--") && arg.includes("=")) {
8
+ const eq = arg.indexOf("=");
9
+ out.push(arg.slice(0, eq), arg.slice(eq + 1));
10
+ } else {
11
+ out.push(arg);
12
+ }
13
+ }
14
+ return out;
15
+ }
16
+
17
+ export function hasHelp(args = []) {
18
+ return args.includes("--help") || args.includes("-h");
19
+ }
20
+
21
+ export function assertKnownFlags(args = [], knownFlags = [], flagsWithValues = []) {
22
+ const known = new Set(knownFlags);
23
+ const valueFlags = new Set(flagsWithValues);
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (valueFlags.has(arg)) {
27
+ i += 1;
28
+ continue;
29
+ }
30
+ if (typeof arg !== "string" || !arg.startsWith("-") || arg === "-") continue;
31
+ if (known.has(arg)) continue;
32
+ failUnknownFlag(arg, known);
33
+ }
34
+ }
35
+
36
+ export function failUnknownFlag(flag, knownFlags = []) {
37
+ const known = [...knownFlags].filter((f) => typeof f === "string" && f.startsWith("-"));
38
+ const closest = closestFlag(flag, known);
39
+ fail({
40
+ code: "UNKNOWN_FLAG",
41
+ message: closest ? `Unknown flag: ${flag}. Did you mean ${closest}?` : `Unknown flag: ${flag}.`,
42
+ details: { flag, closest: closest ? [closest] : [] },
43
+ });
44
+ }
45
+
46
+ export function flagValue(args, flag) {
47
+ const idx = args.indexOf(flag);
48
+ if (idx === -1) return null;
49
+ if (idx + 1 >= args.length) {
50
+ fail({
51
+ code: "BAD_FLAG",
52
+ message: `${flag} requires a value`,
53
+ details: { flag },
54
+ });
55
+ }
56
+ return args[idx + 1];
57
+ }
58
+
59
+ export function parseIntegerFlag(name, value, { min = 1, max = Number.POSITIVE_INFINITY, def } = {}) {
60
+ if (value === undefined || value === null) {
61
+ if (def !== undefined) return def;
62
+ fail({
63
+ code: "BAD_FLAG",
64
+ message: `${name} requires an integer value`,
65
+ details: { flag: name },
66
+ });
67
+ }
68
+ const raw = String(value);
69
+ if (!/^-?\d+$/.test(raw)) {
70
+ fail({
71
+ code: "BAD_FLAG",
72
+ message: `${name} must be an integer, got: ${raw}`,
73
+ details: { flag: name, value: raw },
74
+ });
75
+ }
76
+ const n = Number.parseInt(raw, 10);
77
+ if (n < min) {
78
+ fail({
79
+ code: "BAD_FLAG",
80
+ message: `${name} must be >= ${min}, got: ${n}`,
81
+ details: { flag: name, value: n, min },
82
+ });
83
+ }
84
+ if (n > max) {
85
+ fail({
86
+ code: "BAD_FLAG",
87
+ message: `${name} must be <= ${max}, got: ${n}`,
88
+ details: { flag: name, value: n, max },
89
+ });
90
+ }
91
+ return n;
92
+ }
93
+
94
+ export function failBadProjectId(value) {
95
+ fail({
96
+ code: "BAD_PROJECT_ID",
97
+ message: `Argument '${value}' is not a project id. Project IDs must start with 'prj_'.`,
98
+ hint: "Omit the project id to use the active project, or pass the full prj_... id.",
99
+ details: { value, expected_prefix: "prj_" },
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Validate a webhook URL: parse it locally and reject non-https:// schemes.
105
+ *
106
+ * Scope (GH-192): scheme-only validation. Reject `javascript:`, `file:`,
107
+ * `http:`, `data:`, `ftp:`, etc. before the request leaves the CLI process.
108
+ * Server-side SSRF defenses (private-IP filtering, DNS rebinding, IMDS
109
+ * blocking) live on the gateway, not here — this helper is the cheap
110
+ * client-side guard against the obvious classes.
111
+ *
112
+ * No-op when `url` is null/undefined/empty so callers can pass optional
113
+ * flag values directly. Required-vs-optional handling stays at the call
114
+ * site (e.g. `webhooks register` does its own missing-flag check first).
115
+ *
116
+ * On failure: `fail()` writes the canonical error envelope and exits 1.
117
+ *
118
+ * @param {string|null|undefined} url - The webhook URL to validate.
119
+ * @param {string} fieldName - The CLI flag name for the error envelope (e.g. "--url", "--webhook").
120
+ */
121
+ export function validateWebhookUrl(url, fieldName = "--url") {
122
+ if (!url) return;
123
+ let parsed;
124
+ try {
125
+ parsed = new URL(url);
126
+ } catch {
127
+ fail({
128
+ code: "BAD_WEBHOOK_URL",
129
+ message: `${fieldName} is not a valid URL: ${JSON.stringify(url)}`,
130
+ field: fieldName,
131
+ hint: "Webhook URL must be a fully-qualified https:// URL.",
132
+ details: { flag: fieldName, value: url },
133
+ });
134
+ }
135
+ if (parsed.protocol !== "https:") {
136
+ fail({
137
+ code: "BAD_WEBHOOK_URL",
138
+ message: `${fieldName} must use https://, got ${parsed.protocol}`,
139
+ field: fieldName,
140
+ hint: "Webhook URLs must be https:// for transport security.",
141
+ details: { flag: fieldName, value: url, scheme: parsed.protocol },
142
+ });
143
+ }
144
+ }
145
+
146
+ export function positionalArgs(args = [], flagsWithValues = []) {
147
+ const valueFlags = new Set(flagsWithValues);
148
+ const out = [];
149
+ for (let i = 0; i < args.length; i++) {
150
+ const arg = args[i];
151
+ if (valueFlags.has(arg)) {
152
+ i += 1;
153
+ continue;
154
+ }
155
+ if (typeof arg === "string" && arg.startsWith("-")) continue;
156
+ out.push(arg);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ // Resolve a positional project_id argument with active-project fallback (GH-102, GH-187).
162
+ // If the first positional starts with "prj_", treat it as the project id and
163
+ // strip it from the rest. Otherwise, fall through to the active project from
164
+ // the keystore. Callers can tighten the legacy shorthand when a bare non-prj
165
+ // positional is more likely a mistyped project id than an argument for the
166
+ // active project.
167
+ //
168
+ // Options:
169
+ // rejectBareFirst: when true, error if the first positional
170
+ // is non-empty and doesn't start with "prj_".
171
+ // rejectBareFirstWhenFlagPresent: when one of these flags is present in
172
+ // args AND the first positional doesn't
173
+ // start with "prj_", error out.
174
+ // maxBarePositionals + valueFlags: when set, count the bare (non-flag)
175
+ // positionals using `positionalArgs(args,
176
+ // valueFlags)` and error if the count
177
+ // exceeds maxBarePositionals.
178
+ export function resolvePositionalProject(args, opts = {}) {
179
+ const first = Array.isArray(args) ? args[0] : undefined;
180
+ if (typeof first === "string" && first.startsWith("prj_")) {
181
+ return { projectId: first, rest: args.slice(1) };
182
+ }
183
+ if (
184
+ typeof first === "string" &&
185
+ first.length > 0 &&
186
+ !first.startsWith("-") &&
187
+ Array.isArray(opts.rejectBareFirstWhenFlagPresent) &&
188
+ opts.rejectBareFirstWhenFlagPresent.some((flag) => args.includes(flag))
189
+ ) {
190
+ failBadProjectId(first);
191
+ }
192
+ if (typeof first === "string" && first.length > 0 && !first.startsWith("-") && opts.rejectBareFirst) {
193
+ failBadProjectId(first);
194
+ }
195
+ if (typeof first === "string" && first.length > 0 && !first.startsWith("-") && opts.maxBarePositionals !== undefined) {
196
+ const bare = positionalArgs(args, opts.valueFlags ?? []);
197
+ if (bare.length > opts.maxBarePositionals) {
198
+ failBadProjectId(first);
199
+ }
200
+ }
201
+ return { projectId: resolveProjectId(null), rest: Array.isArray(args) ? args : [] };
202
+ }
203
+
204
+ function closestFlag(flag, candidates) {
205
+ let best = null;
206
+ let bestDistance = Number.POSITIVE_INFINITY;
207
+ for (const candidate of candidates) {
208
+ const d = levenshtein(flag, candidate);
209
+ if (d < bestDistance) {
210
+ best = candidate;
211
+ bestDistance = d;
212
+ }
213
+ }
214
+ if (!best) return null;
215
+ return bestDistance <= 3 ? best : null;
216
+ }
217
+
218
+ function levenshtein(a, b) {
219
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
220
+ const curr = Array.from({ length: b.length + 1 }, () => 0);
221
+ for (let i = 1; i <= a.length; i++) {
222
+ curr[0] = i;
223
+ for (let j = 1; j <= b.length; j++) {
224
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
225
+ curr[j] = Math.min(
226
+ curr[j - 1] + 1,
227
+ prev[j] + 1,
228
+ prev[j - 1] + cost,
229
+ );
230
+ }
231
+ for (let j = 0; j <= b.length; j++) prev[j] = curr[j];
232
+ }
233
+ return prev[b.length];
234
+ }
package/lib/auth.mjs CHANGED
@@ -93,7 +93,7 @@ Notes:
93
93
 
94
94
  Examples:
95
95
  run402 auth settings --allow-password-set true
96
- run402 auth settings --allow-password-set false --project abc123
96
+ run402 auth settings --allow-password-set false --project prj_abc123
97
97
  `,
98
98
  providers: `run402 auth providers — List available auth providers
99
99
 
@@ -105,7 +105,7 @@ Options:
105
105
 
106
106
  Examples:
107
107
  run402 auth providers
108
- run402 auth providers --project abc123
108
+ run402 auth providers --project prj_abc123
109
109
  `,
110
110
  };
111
111
 
@@ -187,10 +187,23 @@ async function settings(args) {
187
187
  message: "Missing --allow-password-set <true|false>",
188
188
  });
189
189
  }
190
+ // Reject anything that isn't literally "true" or "false". Without this guard,
191
+ // the previous `=== "true"` coercion silently turned every other input
192
+ // (including "1", "yes", "TRUE", "bogus") into `false` and printed
193
+ // `{"status":"ok"}`, giving the user the OPPOSITE of what they likely
194
+ // intended for this security-adjacent flag. See GH-204.
195
+ if (allowPasswordSet !== "true" && allowPasswordSet !== "false") {
196
+ fail({
197
+ code: "BAD_FLAG",
198
+ message: "--allow-password-set must be 'true' or 'false'",
199
+ hint: "Use the literal strings 'true' or 'false'.",
200
+ });
201
+ }
190
202
 
203
+ const allow = allowPasswordSet === "true";
191
204
  try {
192
- await getSdk().auth.settings(projectId, { allow_password_set: allowPasswordSet === "true" });
193
- console.log(JSON.stringify({ status: "ok", allow_password_set: allowPasswordSet === "true" }));
205
+ await getSdk().auth.settings(projectId, { allow_password_set: allow });
206
+ console.log(JSON.stringify({ status: "ok", allow_password_set: allow }));
194
207
  } catch (err) {
195
208
  reportSdkError(err);
196
209
  }
package/lib/billing.mjs CHANGED
@@ -84,6 +84,41 @@ Options:
84
84
  Examples:
85
85
  run402 billing history user@example.com
86
86
  run402 billing history 0x1234... --limit 100
87
+ `,
88
+ balance: `run402 billing balance — Show balance for an email or wallet
89
+
90
+ Usage:
91
+ run402 billing balance <identifier>
92
+
93
+ Arguments:
94
+ <identifier> Email address or wallet (0x...)
95
+
96
+ Examples:
97
+ run402 billing balance user@example.com
98
+ run402 billing balance 0x1234abcd...
99
+ `,
100
+ "create-email": `run402 billing create-email — Create an email billing account
101
+
102
+ Usage:
103
+ run402 billing create-email <email>
104
+
105
+ Arguments:
106
+ <email> Email address to register as a billing account
107
+
108
+ Examples:
109
+ run402 billing create-email user@example.com
110
+ `,
111
+ "link-wallet": `run402 billing link-wallet — Link a wallet to an email billing account
112
+
113
+ Usage:
114
+ run402 billing link-wallet <account_id> <wallet>
115
+
116
+ Arguments:
117
+ <account_id> Billing account ID (e.g. acct_abc123)
118
+ <wallet> Wallet address (0x...) to link
119
+
120
+ Examples:
121
+ run402 billing link-wallet acct_abc123 0x1234abcd...
87
122
  `,
88
123
  };
89
124
 
package/lib/blob.mjs CHANGED
@@ -37,6 +37,7 @@ import { pipeline } from "node:stream/promises";
37
37
  import { resolveProject, resolveProjectId, API } from "./config.mjs";
38
38
  import { getSdk } from "./sdk.mjs";
39
39
  import { reportSdkError, fail } from "./sdk-errors.mjs";
40
+ import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag } from "./argparse.mjs";
40
41
 
41
42
  const HELP = `run402 blob — Direct-to-S3 blob storage
42
43
 
@@ -62,13 +63,13 @@ Options:
62
63
  --ttl <seconds> Signed-URL TTL (sign only; default 3600, max 604800)
63
64
 
64
65
  Examples:
65
- run402 blob put ./artifact.tgz --project abc123
66
- run402 blob put ./dist/**/*.png --project abc123 --key assets/
67
- run402 blob put huge.bin --project abc123 --immutable
68
- run402 blob get images/logo.png --output /tmp/logo.png --project abc123
69
- run402 blob ls --project abc123 --prefix images/
70
- run402 blob rm images/logo.png --project abc123
71
- run402 blob sign images/logo.png --project abc123 --ttl 600
66
+ run402 blob put ./artifact.tgz --project prj_abc123
67
+ run402 blob put ./dist/**/*.png --project prj_abc123 --key assets/
68
+ run402 blob put huge.bin --project prj_abc123 --immutable
69
+ run402 blob get images/logo.png --output /tmp/logo.png --project prj_abc123
70
+ run402 blob ls --project prj_abc123 --prefix images/
71
+ run402 blob rm images/logo.png --project prj_abc123
72
+ run402 blob sign images/logo.png --project prj_abc123 --ttl 600
72
73
  `;
73
74
 
74
75
  const SUB_HELP = {
@@ -90,9 +91,9 @@ Options:
90
91
  --json Emit NDJSON progress events on stdout (for agent consumption)
91
92
 
92
93
  Examples:
93
- run402 blob put ./artifact.tgz --project abc123
94
- run402 blob put ./dist/**/*.png --project abc123 --key assets/
95
- run402 blob put huge.bin --project abc123 --immutable --concurrency 8
94
+ run402 blob put ./artifact.tgz --project prj_abc123
95
+ run402 blob put ./dist/**/*.png --project prj_abc123 --key assets/
96
+ run402 blob put huge.bin --project prj_abc123 --immutable --concurrency 8
96
97
  `,
97
98
  get: `run402 blob get — Download a blob by key
98
99
 
@@ -107,7 +108,7 @@ Options:
107
108
  --project <id> Project ID (defaults to active project)
108
109
 
109
110
  Examples:
110
- run402 blob get images/logo.png --output /tmp/logo.png --project abc123
111
+ run402 blob get images/logo.png --output /tmp/logo.png --project prj_abc123
111
112
  `,
112
113
  ls: `run402 blob ls — List blob keys in a project
113
114
 
@@ -120,8 +121,8 @@ Options:
120
121
  --limit <n> Max results (default 100, max 1000)
121
122
 
122
123
  Examples:
123
- run402 blob ls --project abc123
124
- run402 blob ls --project abc123 --prefix images/ --limit 500
124
+ run402 blob ls --project prj_abc123
125
+ run402 blob ls --project prj_abc123 --prefix images/ --limit 500
125
126
  `,
126
127
  rm: `run402 blob rm — Delete a blob
127
128
 
@@ -135,7 +136,7 @@ Options:
135
136
  --project <id> Project ID (defaults to active project)
136
137
 
137
138
  Examples:
138
- run402 blob rm images/logo.png --project abc123
139
+ run402 blob rm images/logo.png --project prj_abc123
139
140
  `,
140
141
  sign: `run402 blob sign — Create a presigned download URL for a blob
141
142
 
@@ -150,7 +151,7 @@ Options:
150
151
  --ttl <seconds> Signed-URL TTL (default 3600, max 604800)
151
152
 
152
153
  Examples:
153
- run402 blob sign reports/2025-q4.pdf --project abc123 --ttl 600
154
+ run402 blob sign reports/2025-q4.pdf --project prj_abc123 --ttl 600
154
155
  `,
155
156
  diagnose: `run402 blob diagnose — Inspect the live CDN state for a public blob URL
156
157
 
@@ -186,7 +187,38 @@ function die(msg, exit_code = 1) {
186
187
  fail({ code: "BAD_USAGE", message: msg, exit_code });
187
188
  }
188
189
 
189
- function parseArgs(args) {
190
+ function dieApiFailure(prefix, http, body) {
191
+ if (body && typeof body === "object" && !Array.isArray(body)) {
192
+ const envelope = { status: "error", http, ...body };
193
+ if (!envelope.message && envelope.error) envelope.message = envelope.error;
194
+ console.error(JSON.stringify(envelope));
195
+ process.exit(1);
196
+ }
197
+ fail({
198
+ message: `${prefix}: HTTP ${http}${typeof body === "string" && body ? `: ${body.slice(0, 500)}` : ""}`,
199
+ details: { http },
200
+ });
201
+ }
202
+
203
+ function parseArgs(rawArgs) {
204
+ const args = normalizeArgv(rawArgs);
205
+ const valueFlags = ["--project", "--key", "--concurrency", "--prefix", "--limit", "--output", "-o", "--ttl"];
206
+ assertKnownFlags(args, [
207
+ "--project",
208
+ "--key",
209
+ "--private",
210
+ "--immutable",
211
+ "--concurrency",
212
+ "--no-resume",
213
+ "--json",
214
+ "--prefix",
215
+ "--limit",
216
+ "--output",
217
+ "-o",
218
+ "--ttl",
219
+ "--help",
220
+ "-h",
221
+ ], valueFlags);
190
222
  const out = { positional: [], project: null, key: null, private: false, immutable: false,
191
223
  concurrency: 4, resume: true, json: false, prefix: null, limit: null,
192
224
  output: null, ttl: null };
@@ -196,13 +228,13 @@ function parseArgs(args) {
196
228
  else if (a === "--key") out.key = args[++i];
197
229
  else if (a === "--private") out.private = true;
198
230
  else if (a === "--immutable") out.immutable = true;
199
- else if (a === "--concurrency") out.concurrency = parseInt(args[++i], 10);
231
+ else if (a === "--concurrency") out.concurrency = parseIntegerFlag("--concurrency", args[++i], { min: 1 });
200
232
  else if (a === "--no-resume") out.resume = false;
201
233
  else if (a === "--json") out.json = true;
202
234
  else if (a === "--prefix") out.prefix = args[++i];
203
- else if (a === "--limit") out.limit = parseInt(args[++i], 10);
235
+ else if (a === "--limit") out.limit = parseIntegerFlag("--limit", args[++i], { min: 1, max: 1000 });
204
236
  else if (a === "--output" || a === "-o") out.output = args[++i];
205
- else if (a === "--ttl") out.ttl = parseInt(args[++i], 10);
237
+ else if (a === "--ttl") out.ttl = parseIntegerFlag("--ttl", args[++i], { min: 1, max: 604800 });
206
238
  else if (!a.startsWith("--")) out.positional.push(a);
207
239
  }
208
240
  return out;
@@ -284,7 +316,7 @@ async function putOne(project, filePath, opts) {
284
316
  immutable: opts.immutable,
285
317
  sha256,
286
318
  });
287
- if (init.status !== 201) die(`Init failed: HTTP ${init.status}: ${JSON.stringify(init.body)}`);
319
+ if (init.status !== 201) dieApiFailure("Init failed", init.status, init.body);
288
320
  initRes = init.body;
289
321
  saveState({
290
322
  upload_id: initRes.upload_id,
@@ -330,7 +362,7 @@ async function putOne(project, filePath, opts) {
330
362
  ? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag })) }
331
363
  : {};
332
364
  const complete = await apiFetch(`${API}/storage/v1/uploads/${state.upload_id}/complete`, "POST", project, body);
333
- if (complete.status !== 200) die(`Complete failed: HTTP ${complete.status}: ${JSON.stringify(complete.body)}`);
365
+ if (complete.status !== 200) dieApiFailure("Complete failed", complete.status, complete.body);
334
366
 
335
367
  removeState(state.upload_id);
336
368
  log(opts, { event: "done", ...complete.body });
@@ -565,7 +597,8 @@ export async function run(sub, args) {
565
597
  console.log(HELP);
566
598
  process.exit(0);
567
599
  }
568
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
600
+ args = normalizeArgv(args);
601
+ if (Array.isArray(args) && hasHelp(args)) {
569
602
  console.log(SUB_HELP[sub] || HELP);
570
603
  process.exit(0);
571
604
  }
package/lib/config.mjs CHANGED
@@ -14,8 +14,27 @@ export const ALLOWANCE_FILE = getAllowancePath();
14
14
  export const PROJECTS_FILE = getKeystorePath();
15
15
  export const API = getApiBase();
16
16
 
17
+ /**
18
+ * Wraps core's `readAllowance()` and converts the malformed-shape throw
19
+ * (GH-194) into the canonical CLI failure envelope. Without this guard, every
20
+ * CLI subcommand that touches the allowance leaks a Node stack trace and
21
+ * source paths the moment a user has a malformed `allowance.json`.
22
+ *
23
+ * The unparseable-JSON case still returns `null` (matching the historical
24
+ * "no_allowance" UX); only valid-JSON-but-wrong-shape becomes a structured
25
+ * error with `code: BAD_ALLOWANCE_FILE`.
26
+ */
17
27
  export function readAllowance() {
18
- return coreReadAllowance();
28
+ try {
29
+ return coreReadAllowance();
30
+ } catch (err) {
31
+ fail({
32
+ code: "BAD_ALLOWANCE_FILE",
33
+ message: err?.message ?? "allowance.json is malformed",
34
+ hint: "Back up ~/.config/run402/allowance.json and run 'run402 init' to recreate it.",
35
+ details: { path: ALLOWANCE_FILE },
36
+ });
37
+ }
19
38
  }
20
39
 
21
40
  export function saveAllowance(data) {
package/lib/contracts.mjs CHANGED
@@ -83,6 +83,47 @@ Usage:
83
83
 
84
84
  Usage:
85
85
  run402 contracts delete <project_id> <wallet_id> --confirm
86
+ `,
87
+ "get-wallet": `run402 contracts get-wallet — Get wallet metadata + live balance
88
+
89
+ Usage:
90
+ run402 contracts get-wallet <project_id> <wallet_id>
91
+
92
+ Arguments:
93
+ <project_id> Project ID that owns the wallet
94
+ <wallet_id> Wallet ID (e.g. cwlt_abc123)
95
+
96
+ Examples:
97
+ run402 contracts get-wallet prj_abc123 cwlt_abc123
98
+ `,
99
+ "list-wallets": `run402 contracts list-wallets — List all KMS wallets for a project
100
+
101
+ Usage:
102
+ run402 contracts list-wallets <project_id>
103
+
104
+ Arguments:
105
+ <project_id> Project ID to list wallets for
106
+
107
+ Notes:
108
+ - Includes deleted wallets
109
+
110
+ Examples:
111
+ run402 contracts list-wallets prj_abc123
112
+ `,
113
+ status: `run402 contracts status — Get a contract call's status and receipt
114
+
115
+ Usage:
116
+ run402 contracts status <project_id> <call_id>
117
+
118
+ Arguments:
119
+ <project_id> Project ID that submitted the call
120
+ <call_id> Contract call ID returned from 'contracts call'
121
+
122
+ Notes:
123
+ - Returns status, gas used, gas cost (USD-micros), and receipt
124
+
125
+ Examples:
126
+ run402 contracts status prj_abc123 ccall_abc123
86
127
  `,
87
128
  };
88
129