run402 1.54.2 → 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.
package/lib/functions.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "fs";
1
+ import { readFileSync, existsSync, statSync } from "fs";
2
2
  import { findProject, API } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail } from "./sdk-errors.mjs";
@@ -141,6 +141,29 @@ Examples:
141
141
  run402 functions update prj_abc123 send-reminders --schedule '0 */4 * * *'
142
142
  run402 functions update prj_abc123 send-reminders --schedule-remove
143
143
  run402 functions update prj_abc123 my-func --timeout 15 --memory 256
144
+ `,
145
+ list: `run402 functions list — List all functions for a project
146
+
147
+ Usage:
148
+ run402 functions list <project_id>
149
+
150
+ Arguments:
151
+ <project_id> Target project ID
152
+
153
+ Examples:
154
+ run402 functions list prj_abc123
155
+ `,
156
+ delete: `run402 functions delete — Delete a function from a project
157
+
158
+ Usage:
159
+ run402 functions delete <project_id> <name>
160
+
161
+ Arguments:
162
+ <project_id> Target project ID
163
+ <name> Function name to delete
164
+
165
+ Examples:
166
+ run402 functions delete prj_abc123 stripe-webhook
144
167
  `,
145
168
  };
146
169
 
@@ -158,6 +181,24 @@ async function deploy(projectId, name, args) {
158
181
  if (!opts.file) {
159
182
  fail({ code: "BAD_USAGE", message: "Missing --file <file>" });
160
183
  }
184
+ if (!existsSync(opts.file)) {
185
+ fail({
186
+ code: "FILE_NOT_FOUND",
187
+ message: `File not found: ${opts.file}`,
188
+ field: "--file",
189
+ path: opts.file,
190
+ hint: "Check that --file points to an existing source file.",
191
+ });
192
+ }
193
+ const stat = statSync(opts.file);
194
+ if (!stat.isFile()) {
195
+ fail({
196
+ code: "NOT_A_FILE",
197
+ message: `--file points to a ${stat.isDirectory() ? "directory" : "non-regular file"}: ${opts.file}`,
198
+ field: "--file",
199
+ path: opts.file,
200
+ });
201
+ }
161
202
  const code = readFileSync(opts.file, "utf-8");
162
203
 
163
204
  const deployOpts = { name, code };
package/lib/image.mjs CHANGED
@@ -27,12 +27,44 @@ Notes:
27
27
  - Use --output to save directly to a file instead of printing base64
28
28
  `;
29
29
 
30
+ const SUB_HELP = {
31
+ generate: `run402 image generate — Generate an AI image from a text prompt
32
+
33
+ Usage:
34
+ run402 image generate "<prompt>" [options]
35
+
36
+ Arguments:
37
+ <prompt> Text prompt describing the image (quote it)
38
+
39
+ Options:
40
+ --aspect <ratio> Image aspect ratio: square | landscape | portrait
41
+ (default: square)
42
+ --output <file> Save image to file (e.g. output.png). If omitted,
43
+ returns base64 JSON to stdout.
44
+
45
+ Notes:
46
+ - Requires a funded allowance (run402 allowance create && run402 allowance fund)
47
+ - Payments are processed automatically via x402 micropayments
48
+ - Use --output to save directly to a file instead of printing base64
49
+
50
+ Examples:
51
+ run402 image generate "a startup mascot, pixel art"
52
+ run402 image generate "futuristic city at night" --aspect landscape
53
+ run402 image generate "portrait of a cat CEO" --aspect portrait --output cat.png
54
+ `,
55
+ };
56
+
30
57
  export async function run(sub, args) {
31
58
  if (!sub || sub === '--help' || sub === '-h') {
32
59
  console.log(HELP);
33
60
  process.exit(0);
34
61
  }
35
62
 
63
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
64
+ console.log(SUB_HELP[sub] || HELP);
65
+ process.exit(0);
66
+ }
67
+
36
68
  if (sub !== "generate") {
37
69
  console.error(`Unknown subcommand: ${sub}\n`);
38
70
  console.log(HELP);
@@ -43,7 +75,7 @@ export async function run(sub, args) {
43
75
  let i = 0;
44
76
  if (i < args.length && !args[i].startsWith("--")) opts.prompt = args[i++];
45
77
  while (i < args.length) {
46
- if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
78
+ if (args[i] === "--help" || args[i] === "-h") { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
47
79
  else if (args[i] === "--aspect" && args[i + 1]) { opts.aspect = args[++i]; }
48
80
  else if (args[i] === "--output" && args[i + 1]) { opts.output = args[++i]; }
49
81
  i++;
package/lib/message.mjs CHANGED
@@ -10,21 +10,68 @@ Usage:
10
10
  Notes:
11
11
  - Requires an active tier (run402 tier set <tier>)
12
12
  - Requires an allowance (run402 allowance create)
13
+ - Messages are capped at 8 KB (8192 bytes UTF-8) to keep the developer
14
+ inbox useful and prevent payload-dump misuse. Trim or summarize long
15
+ content (e.g. stack traces) before sending.
13
16
 
14
17
  Examples:
15
18
  run402 message send "Hello from my agent!"
16
19
  `;
17
20
 
21
+ // Cap message body at a Twitter-ish but engineer-generous size: enough for
22
+ // a few paragraphs and a stack-trace excerpt, small enough that a misbehaving
23
+ // agent script can't dump arbitrary content into the developer inbox in one
24
+ // call. UTF-8 bytes (not characters) — emoji and accented chars count as
25
+ // multiple bytes.
26
+ const MESSAGE_MAX_BYTES = 8192;
27
+
28
+ const SUB_HELP = {
29
+ send: `run402 message send — Send a message to Run402 developers
30
+
31
+ Usage:
32
+ run402 message send <text>
33
+
34
+ Arguments:
35
+ <text> Message body (quote it; remaining args are joined with
36
+ spaces if multiple positional words are provided)
37
+
38
+ Notes:
39
+ - Requires an active tier (run402 tier set <tier>)
40
+ - Requires an allowance (run402 allowance create)
41
+ - Messages are capped at 8 KB (8192 bytes UTF-8) to keep the developer
42
+ inbox useful and prevent payload-dump misuse.
43
+
44
+ Examples:
45
+ run402 message send "Hello from my agent!"
46
+ `,
47
+ };
48
+
18
49
  async function send(text) {
19
- if (!text) {
50
+ if (!text || typeof text !== "string") {
20
51
  fail({ code: "BAD_USAGE", message: "Missing message text." });
21
52
  }
53
+ // Cap check runs BEFORE the allowance check so oversized payloads surface
54
+ // a structured size error instead of being masked by a missing-allowance
55
+ // exit.
56
+ const bytes = Buffer.byteLength(text, "utf-8");
57
+ if (bytes > MESSAGE_MAX_BYTES) {
58
+ fail({
59
+ code: "MESSAGE_TOO_LONG",
60
+ message: `Message is ${bytes} bytes; maximum is ${MESSAGE_MAX_BYTES} bytes (~8 KB).`,
61
+ hint: "Trim or summarize the message.",
62
+ details: { bytes, max_bytes: MESSAGE_MAX_BYTES },
63
+ });
64
+ }
22
65
  // Preserve the aggressive early exit when no allowance is configured.
23
66
  allowanceAuthHeaders("/message/v1");
24
67
 
25
68
  try {
26
69
  await getSdk().admin.sendMessage(text);
27
- console.log(JSON.stringify({ status: "ok", message: "Message sent to Run402 developers." }));
70
+ console.log(JSON.stringify({
71
+ status: "ok",
72
+ message: "Message sent to Run402 developers.",
73
+ bytes_sent: bytes,
74
+ }));
28
75
  } catch (err) {
29
76
  reportSdkError(err);
30
77
  }
@@ -33,7 +80,7 @@ async function send(text) {
33
80
  export async function run(sub, args) {
34
81
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
35
82
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
36
- console.log(HELP);
83
+ console.log(SUB_HELP[sub] || HELP);
37
84
  process.exit(0);
38
85
  }
39
86
  if (sub !== "send") {
package/lib/projects.mjs CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "fs";
2
2
  import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
5
- import { assertKnownFlags, failBadProjectId, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
5
+ import { assertKnownFlags, failBadProjectId, hasHelp, normalizeArgv, positionalArgs, resolvePositionalProject } from "./argparse.mjs";
6
6
 
7
7
  const HELP = `run402 projects — Manage your deployed Run402 projects
8
8
 
@@ -123,7 +123,37 @@ async function provision(args) {
123
123
  const opts = { tier: "prototype", name: undefined };
124
124
  for (let i = 0; i < args.length; i++) {
125
125
  if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
126
- if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
126
+ // Use !== undefined so an empty-string value is captured (and rejected
127
+ // below) rather than silently dropped by the falsy-check pattern (GH-176).
128
+ if (args[i] === "--name" && args[i + 1] !== undefined) opts.name = args[++i];
129
+ }
130
+ // Validate --name when provided. Omitted --name lets the server pick a
131
+ // default. The same envelope should also be enforced server-side (GH-176).
132
+ if (opts.name !== undefined) {
133
+ if (opts.name === "") {
134
+ fail({
135
+ code: "BAD_PROJECT_NAME",
136
+ message: "--name must not be empty.",
137
+ details: { field: "--name" },
138
+ hint: "Provide a 1-128 character name, or omit --name to use the server-assigned default.",
139
+ });
140
+ }
141
+ if (opts.name.length > 128) {
142
+ fail({
143
+ code: "BAD_PROJECT_NAME",
144
+ message: `--name must be 1-128 characters, got ${opts.name.length}.`,
145
+ details: { field: "--name", length: opts.name.length, max: 128 },
146
+ });
147
+ }
148
+ // eslint-disable-next-line no-control-regex
149
+ if (/[\x00-\x1f\x7f]/.test(opts.name)) {
150
+ fail({
151
+ code: "BAD_PROJECT_NAME",
152
+ message: "--name contains control characters (newline, tab, etc).",
153
+ details: { field: "--name" },
154
+ hint: "Project names should be a single-line label.",
155
+ });
156
+ }
127
157
  }
128
158
  // Preserve the aggressive early exit when no allowance is configured —
129
159
  // gives the user a more specific prompt than the SDK's 401/402 path.
@@ -259,6 +289,13 @@ async function sqlCmd(projectId, args = []) {
259
289
  }
260
290
 
261
291
  async function rest(projectId, table, queryParams) {
292
+ if (!table) {
293
+ fail({
294
+ code: "BAD_USAGE",
295
+ message: "Missing <table> argument. Usage: run402 projects rest [id] <table> [\"<query>\"]",
296
+ hint: "Run 'run402 projects schema <id>' to list tables.",
297
+ });
298
+ }
262
299
  const p = findProject(projectId);
263
300
  const res = await fetch(`${API}/rest/v1/${table}${queryParams ? '?' + queryParams : ''}`, { headers: { "apikey": p.anon_key } });
264
301
  const data = await res.json();
@@ -373,35 +410,6 @@ async function deleteProject(projectId, args = []) {
373
410
  }
374
411
  }
375
412
 
376
- // Resolve a positional project_id argument with active-project fallback (GH-102).
377
- // Callers can tighten the legacy shorthand when a bare non-prj positional is
378
- // more likely a mistyped project id than an argument for the active project.
379
- function resolvePositionalProject(args, opts = {}) {
380
- const first = Array.isArray(args) ? args[0] : undefined;
381
- if (typeof first === "string" && first.startsWith("prj_")) {
382
- return { projectId: first, rest: args.slice(1) };
383
- }
384
- if (
385
- typeof first === "string" &&
386
- first.length > 0 &&
387
- !first.startsWith("-") &&
388
- Array.isArray(opts.rejectBareFirstWhenFlagPresent) &&
389
- opts.rejectBareFirstWhenFlagPresent.some((flag) => args.includes(flag))
390
- ) {
391
- failBadProjectId(first);
392
- }
393
- if (typeof first === "string" && first.length > 0 && !first.startsWith("-") && opts.rejectBareFirst) {
394
- failBadProjectId(first);
395
- }
396
- if (typeof first === "string" && first.length > 0 && !first.startsWith("-") && opts.maxBarePositionals !== undefined) {
397
- const bare = positionalArgs(args, opts.valueFlags ?? []);
398
- if (bare.length > opts.maxBarePositionals) {
399
- failBadProjectId(first);
400
- }
401
- }
402
- return { projectId: resolveProjectId(null), rest: Array.isArray(args) ? args : [] };
403
- }
404
-
405
413
  const FLAGS_BY_SUB = {
406
414
  provision: { known: ["--tier", "--name"], values: ["--tier", "--name"] },
407
415
  sql: { known: ["--file", "--params"], values: ["--file", "--params"] },
@@ -29,7 +29,7 @@
29
29
  * validation: the call wasn't sent, so retrying is safe and won't help unless
30
30
  * the user fixes input.
31
31
  */
32
- export function fail({ message, code, hint, details, next_actions, retryable = false, safe_to_retry = true, exit_code = 1 } = {}) {
32
+ export function fail({ message, code, hint, details, next_actions, field, retryable = false, safe_to_retry = true, exit_code = 1 } = {}) {
33
33
  const envelope = {
34
34
  status: "error",
35
35
  code: code ?? "BAD_USAGE",
@@ -39,6 +39,7 @@ export function fail({ message, code, hint, details, next_actions, retryable = f
39
39
  };
40
40
  if (hint !== undefined) envelope.hint = hint;
41
41
  if (details !== undefined) envelope.details = details;
42
+ if (field !== undefined) envelope.field = field;
42
43
  envelope.next_actions = Array.isArray(next_actions) ? next_actions : [];
43
44
  envelope.trace_id = null;
44
45
  console.error(JSON.stringify(envelope));
package/lib/secrets.mjs CHANGED
@@ -45,6 +45,33 @@ Notes:
45
45
  Examples:
46
46
  run402 secrets set prj_abc123 STRIPE_KEY sk-1234
47
47
  run402 secrets set prj_abc123 TLS_CERT --file cert.pem
48
+ `,
49
+ list: `run402 secrets list — List all secrets for a project
50
+
51
+ Usage:
52
+ run402 secrets list <id>
53
+
54
+ Arguments:
55
+ <id> Project ID (from 'run402 projects list')
56
+
57
+ Notes:
58
+ - Returns secret keys with a value_hash (first 8 hex chars of SHA-256)
59
+ for verifying the correct value was set; raw values are write-only
60
+
61
+ Examples:
62
+ run402 secrets list prj_abc123
63
+ `,
64
+ delete: `run402 secrets delete — Delete a secret from a project
65
+
66
+ Usage:
67
+ run402 secrets delete <id> <key>
68
+
69
+ Arguments:
70
+ <id> Project ID (from 'run402 projects list')
71
+ <key> Secret key name to remove
72
+
73
+ Examples:
74
+ run402 secrets delete prj_abc123 STRIPE_KEY
48
75
  `,
49
76
  };
50
77
 
@@ -22,6 +22,83 @@ Examples:
22
22
  run402 sender-domain inbound-disable kysigned.com
23
23
  `;
24
24
 
25
+ const SUB_HELP = {
26
+ register: `run402 sender-domain register — Register a custom sender domain
27
+
28
+ Usage:
29
+ run402 sender-domain register <domain> [--project <id>]
30
+
31
+ Arguments:
32
+ <domain> Custom sender domain (e.g. kysigned.com)
33
+
34
+ Options:
35
+ --project <id> Project ID (defaults to the active project)
36
+
37
+ Notes:
38
+ - Returns DNS records (DKIM, SPF, DMARC) to add at your DNS provider
39
+ - Use 'run402 sender-domain status' to poll until verified
40
+
41
+ Examples:
42
+ run402 sender-domain register kysigned.com
43
+ run402 sender-domain register kysigned.com --project prj_abc123
44
+ `,
45
+ status: `run402 sender-domain status — Check verification status of the project's sender domain
46
+
47
+ Usage:
48
+ run402 sender-domain status [--project <id>]
49
+
50
+ Options:
51
+ --project <id> Project ID (defaults to the active project)
52
+
53
+ Examples:
54
+ run402 sender-domain status
55
+ run402 sender-domain status --project prj_abc123
56
+ `,
57
+ remove: `run402 sender-domain remove — Remove the project's custom sender domain
58
+
59
+ Usage:
60
+ run402 sender-domain remove [--project <id>]
61
+
62
+ Options:
63
+ --project <id> Project ID (defaults to the active project)
64
+
65
+ Examples:
66
+ run402 sender-domain remove
67
+ run402 sender-domain remove --project prj_abc123
68
+ `,
69
+ "inbound-enable": `run402 sender-domain inbound-enable — Enable inbound email for a sender domain
70
+
71
+ Usage:
72
+ run402 sender-domain inbound-enable <domain> [--project <id>]
73
+
74
+ Arguments:
75
+ <domain> Custom sender domain to enable inbound on
76
+
77
+ Options:
78
+ --project <id> Project ID (defaults to the active project)
79
+
80
+ Notes:
81
+ - Requires the domain to be DKIM-verified first
82
+
83
+ Examples:
84
+ run402 sender-domain inbound-enable kysigned.com
85
+ `,
86
+ "inbound-disable": `run402 sender-domain inbound-disable — Disable inbound email for a sender domain
87
+
88
+ Usage:
89
+ run402 sender-domain inbound-disable <domain> [--project <id>]
90
+
91
+ Arguments:
92
+ <domain> Custom sender domain to disable inbound on
93
+
94
+ Options:
95
+ --project <id> Project ID (defaults to the active project)
96
+
97
+ Examples:
98
+ run402 sender-domain inbound-disable kysigned.com
99
+ `,
100
+ };
101
+
25
102
  function parseFlag(args, flag) {
26
103
  for (let i = 0; i < args.length; i++) {
27
104
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -106,7 +183,7 @@ async function inboundToggle(action, args) {
106
183
 
107
184
  export async function run(sub, args) {
108
185
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
109
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(HELP); process.exit(0); }
186
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
110
187
  switch (sub) {
111
188
  case "register": await register(args); break;
112
189
  case "status": await status(args); break;
package/lib/service.mjs CHANGED
@@ -13,6 +13,35 @@ Notes:
13
13
  balance, tier, projects), use 'run402 status'.
14
14
  `;
15
15
 
16
+ const SUB_HELP = {
17
+ status: `run402 service status — Public service availability report
18
+
19
+ Usage:
20
+ run402 service status
21
+
22
+ Notes:
23
+ - Unauthenticated and free; no allowance required
24
+ - Returns uptime, supported capabilities, operator, and deployment info
25
+ - For account state (allowance, balance, tier, projects), use
26
+ 'run402 status' instead
27
+
28
+ Examples:
29
+ run402 service status
30
+ `,
31
+ health: `run402 service health — Service liveness check
32
+
33
+ Usage:
34
+ run402 service health
35
+
36
+ Notes:
37
+ - Unauthenticated and free; no allowance required
38
+ - Returns per-dependency status and the deployed version
39
+
40
+ Examples:
41
+ run402 service health
42
+ `,
43
+ };
44
+
16
45
  async function status() {
17
46
  try {
18
47
  const data = await getSdk().service.status();
@@ -34,7 +63,7 @@ async function health() {
34
63
  export async function run(sub, args) {
35
64
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
36
65
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
37
- console.log(HELP);
66
+ console.log(SUB_HELP[sub] || HELP);
38
67
  process.exit(0);
39
68
  }
40
69
  switch (sub) {
@@ -48,6 +48,35 @@ Notes:
48
48
  Examples:
49
49
  run402 subdomains claim myapp
50
50
  run402 subdomains claim myapp --deployment dpl_abc123 --project prj_abc123
51
+ `,
52
+ list: `run402 subdomains list — List subdomains claimed by a project
53
+
54
+ Usage:
55
+ run402 subdomains list [<id>]
56
+
57
+ Arguments:
58
+ <id> Project ID (defaults to the active project)
59
+
60
+ Examples:
61
+ run402 subdomains list
62
+ run402 subdomains list prj_abc123
63
+ `,
64
+ delete: `run402 subdomains delete — Release a claimed subdomain
65
+
66
+ Usage:
67
+ run402 subdomains delete <name> --confirm [--project <id>]
68
+
69
+ Arguments:
70
+ <name> Subdomain name to release
71
+
72
+ Options:
73
+ --confirm Required: releasing a subdomain is irreversible and
74
+ makes it available for any other project to claim
75
+ --project <id> Project ID (defaults to the active project)
76
+
77
+ Examples:
78
+ run402 subdomains delete myapp --confirm
79
+ run402 subdomains delete myapp --confirm --project prj_abc123
51
80
  `,
52
81
  };
53
82
 
package/lib/tier.mjs CHANGED
@@ -24,6 +24,46 @@ Examples:
24
24
  run402 tier set hobby
25
25
  `;
26
26
 
27
+ const SUB_HELP = {
28
+ status: `run402 tier status — Show current tier subscription state
29
+
30
+ Usage:
31
+ run402 tier status
32
+
33
+ Notes:
34
+ - Returns the current tier name, status, and expiry
35
+ - Use 'run402 tier set <tier>' to subscribe, renew, or upgrade
36
+
37
+ Examples:
38
+ run402 tier status
39
+ `,
40
+ set: `run402 tier set — Subscribe, renew, or upgrade your tier
41
+
42
+ Usage:
43
+ run402 tier set <tier>
44
+
45
+ Arguments:
46
+ <tier> One of: prototype, hobby, team
47
+
48
+ Tiers:
49
+ prototype $0.10/7d (free with testnet faucet)
50
+ hobby $5/30d
51
+ team $20/30d
52
+
53
+ Notes:
54
+ Server auto-detects action based on current allowance state:
55
+ - No tier or expired -> subscribe
56
+ - Same tier, active -> renew (extends from expiry)
57
+ - Higher tier -> upgrade (prorated refund to allowance)
58
+ - Lower tier, active -> rejected (wait for expiry)
59
+ Pays via x402 micropayments.
60
+
61
+ Examples:
62
+ run402 tier set prototype
63
+ run402 tier set hobby
64
+ `,
65
+ };
66
+
27
67
  async function status() {
28
68
  try {
29
69
  const data = await getSdk().tier.status();
@@ -55,7 +95,7 @@ export async function run(sub, args) {
55
95
  process.exit(0);
56
96
  }
57
97
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
58
- console.log(HELP);
98
+ console.log(SUB_HELP[sub] || HELP);
59
99
  process.exit(0);
60
100
  }
61
101
  switch (sub) {
package/lib/webhooks.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
+ import { validateWebhookUrl } from "./argparse.mjs";
4
5
 
5
6
  const HELP = `run402 email webhooks — Manage mailbox webhooks
6
7
 
@@ -121,6 +122,11 @@ async function update(args) {
121
122
  if (!url && !eventsRaw) {
122
123
  fail({ code: "BAD_USAGE", message: "Provide at least --url or --events" });
123
124
  }
125
+ // GH-192: scheme-only local validation. Server-side SSRF defenses are out
126
+ // of scope for the CLI (private-IP / DNS rebinding / IMDS belongs on the
127
+ // gateway). `validateWebhookUrl` is a no-op when `url` is null/undefined,
128
+ // so partial updates that change only `--events` still work.
129
+ validateWebhookUrl(url, "--url");
124
130
 
125
131
  try {
126
132
  const data = await getSdk().email.webhooks.update(projectId, webhookId, {
@@ -146,6 +152,10 @@ async function register(args) {
146
152
  hint: "run402 email webhooks register --url <url> --events <e1,e2>",
147
153
  });
148
154
  }
155
+ // GH-192: validate scheme locally before any network call. Catches
156
+ // javascript:/file:/http:/data: schemes that the gateway would reject
157
+ // anyway, but with a friendlier round-trip-free error.
158
+ validateWebhookUrl(url, "--url");
149
159
  if (!eventsRaw) {
150
160
  fail({
151
161
  code: "BAD_USAGE",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.54.2",
3
+ "version": "1.54.3",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,6 +87,11 @@ export function formatSIWEMessage(opts, address) {
87
87
  * @param path - API path (e.g. "/projects/v1") used to build the SIWE uri field.
88
88
  */
89
89
  export function getAllowanceAuthHeaders(path, allowancePath) {
90
+ // GH-194: readAllowance throws on a malformed-shape allowance file. The
91
+ // CLI's higher-level readAllowance wrapper surfaces this as a structured
92
+ // BAD_ALLOWANCE_FILE envelope; here we preserve the public contract that
93
+ // this helper returns SIWxAuthHeaders | null. Re-throw so callers above
94
+ // the CLI's wrapper (e.g. SDK paid-fetch) can decide whether to swallow it.
90
95
  const allowance = readAllowance(allowancePath);
91
96
  if (!allowance || !allowance.address || !allowance.privateKey)
92
97
  return null;