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/cli.mjs +18 -0
- package/core-dist/allowance-auth.js +5 -0
- package/core-dist/allowance.js +49 -1
- package/core-dist/config.js +35 -2
- package/core-dist/wallet-auth.js +62 -0
- package/core-dist/wallet.js +25 -0
- package/lib/agent.mjs +29 -1
- package/lib/ai.mjs +113 -37
- package/lib/apps.mjs +34 -0
- package/lib/argparse.mjs +87 -0
- package/lib/auth.mjs +15 -2
- package/lib/billing.mjs +35 -0
- package/lib/config.mjs +20 -1
- package/lib/contracts.mjs +41 -0
- package/lib/deploy.mjs +125 -58
- package/lib/domains.mjs +79 -5
- package/lib/email.mjs +34 -0
- package/lib/functions.mjs +42 -1
- package/lib/image.mjs +33 -1
- package/lib/message.mjs +50 -3
- package/lib/projects.mjs +39 -31
- package/lib/sdk-errors.mjs +2 -1
- package/lib/secrets.mjs +27 -0
- package/lib/sender-domain.mjs +78 -1
- package/lib/service.mjs +30 -1
- package/lib/subdomains.mjs +29 -0
- package/lib/tier.mjs +41 -1
- package/lib/webhooks.mjs +10 -0
- package/package.json +1 -1
- package/sdk/core-dist/allowance-auth.js +5 -0
- package/sdk/core-dist/allowance.js +49 -1
- package/sdk/core-dist/config.js +35 -2
- package/sdk/core-dist/wallet-auth.js +62 -0
- package/sdk/core-dist/wallet.js +25 -0
- package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
- package/sdk/dist/node/paid-fetch.js +12 -1
- package/sdk/dist/node/paid-fetch.js.map +1 -1
package/lib/argparse.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fail } from "./sdk-errors.mjs";
|
|
2
|
+
import { resolveProjectId } from "./config.mjs";
|
|
2
3
|
|
|
3
4
|
export function normalizeArgv(argv = []) {
|
|
4
5
|
const out = [];
|
|
@@ -99,6 +100,49 @@ export function failBadProjectId(value) {
|
|
|
99
100
|
});
|
|
100
101
|
}
|
|
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
|
+
|
|
102
146
|
export function positionalArgs(args = [], flagsWithValues = []) {
|
|
103
147
|
const valueFlags = new Set(flagsWithValues);
|
|
104
148
|
const out = [];
|
|
@@ -114,6 +158,49 @@ export function positionalArgs(args = [], flagsWithValues = []) {
|
|
|
114
158
|
return out;
|
|
115
159
|
}
|
|
116
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
|
+
|
|
117
204
|
function closestFlag(flag, candidates) {
|
|
118
205
|
let best = null;
|
|
119
206
|
let bestDistance = Number.POSITIVE_INFINITY;
|
package/lib/auth.mjs
CHANGED
|
@@ -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:
|
|
193
|
-
console.log(JSON.stringify({ status: "ok", allow_password_set:
|
|
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/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
|
-
|
|
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
|
|
package/lib/deploy.mjs
CHANGED
|
@@ -16,77 +16,100 @@ Options:
|
|
|
16
16
|
--project <id> Project ID to deploy to (default: active project)
|
|
17
17
|
--help, -h Show this help message
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Subcommands (recommended for new manifests):
|
|
20
|
+
run402 deploy apply --manifest <file> unified deploy primitive (v1.34+)
|
|
21
|
+
run402 deploy resume <operation_id> resume a stuck operation
|
|
22
|
+
run402 deploy list [--project <id>] list recent deploy operations
|
|
23
|
+
run402 deploy events <operation_id> fetch event stream for an operation
|
|
24
|
+
|
|
25
|
+
Manifest format (JSON, v2 ReleaseSpec — recommended):
|
|
20
26
|
{
|
|
21
27
|
"project_id": "prj_...",
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
"database": {
|
|
29
|
+
"migrations": [
|
|
30
|
+
{ "id": "001_init", "sql": "CREATE TABLE IF NOT EXISTS items (...)" }
|
|
31
|
+
],
|
|
32
|
+
"expose": {
|
|
33
|
+
"version": "1",
|
|
34
|
+
"tables": [
|
|
35
|
+
{ "name": "items", "expose": true, "policy": "public_read_authenticated_write" }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"secrets": { "set": { "OPENAI_API_KEY": { "value": "sk-..." } } },
|
|
40
|
+
"functions": {
|
|
41
|
+
"replace": {
|
|
42
|
+
"api": {
|
|
43
|
+
"runtime": "node22",
|
|
44
|
+
"source": { "data": "export default async (req) => new Response('ok')" }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"site": {
|
|
49
|
+
"replace": {
|
|
50
|
+
"index.html": { "data": "<!doctype html><html>...</html>" },
|
|
51
|
+
"assets/logo.png": { "data": "iVBORw0KGgo...", "encoding": "base64" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"subdomains": { "set": ["my-app"] }
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
project_id is required (provision first with 'run402 provision').
|
|
41
|
-
All other fields are optional.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Binary files (images, fonts,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
INSERT/UPDATE/DELETE any row (not just
|
|
72
|
-
their own). For collaborative content
|
|
73
|
-
like shared boards or announcements.
|
|
74
|
-
public_read_write_UNRESTRICTED ⚠ fully open — anon_key can read AND
|
|
75
|
-
write any row. Only for intentionally
|
|
76
|
-
public tables (guestbooks, waitlists,
|
|
77
|
-
feedback forms). REQUIRES
|
|
58
|
+
All other fields are optional. Top-level absence = "leave untouched".
|
|
59
|
+
|
|
60
|
+
Source of truth for the v2 ReleaseSpec shape:
|
|
61
|
+
https://run402.com/llms-cli.txt (search for "Unified Deploy")
|
|
62
|
+
|
|
63
|
+
Replace vs patch semantics per resource:
|
|
64
|
+
"site": { "replace": {...} } whole-site (omitted files removed)
|
|
65
|
+
"site": { "patch": { "put": {...}, "delete": [...] } } surgical updates
|
|
66
|
+
Same for "functions" and "secrets". Migrations are always additive (each
|
|
67
|
+
is keyed by id; re-shipping the same id+sql is a registry noop, same id
|
|
68
|
+
with different sql is a hard MIGRATION_CHECKSUM_MISMATCH error).
|
|
69
|
+
|
|
70
|
+
File entries accept inline "data", a local "path", or a "sql_path"
|
|
71
|
+
(migrations only) — paths are resolved relative to the manifest file's
|
|
72
|
+
directory. Binary files (images, fonts, PDFs) take "encoding": "base64";
|
|
73
|
+
text defaults to UTF-8.
|
|
74
|
+
|
|
75
|
+
Authorization (database.expose):
|
|
76
|
+
Tables are dark by default — anon/authenticated can't read them until
|
|
77
|
+
you declare them via "database.expose". Per-table policies:
|
|
78
|
+
user_owns_rows users see only their own rows.
|
|
79
|
+
Requires "owner_column"; with
|
|
80
|
+
"force_owner_on_insert": true the
|
|
81
|
+
gateway sets it from auth.uid()
|
|
82
|
+
automatically.
|
|
83
|
+
public_read_authenticated_write anyone reads; any authenticated
|
|
84
|
+
user can INSERT/UPDATE/DELETE any
|
|
85
|
+
row (not just their own).
|
|
86
|
+
public_read_write_UNRESTRICTED ⚠ fully open — anon_key reads AND
|
|
87
|
+
writes. REQUIRES
|
|
78
88
|
"i_understand_this_is_unrestricted":
|
|
79
89
|
true on the table entry.
|
|
80
|
-
custom escape hatch. Provide "custom_sql"
|
|
81
|
-
CREATE POLICY statements.
|
|
90
|
+
custom escape hatch. Provide "custom_sql"
|
|
91
|
+
with CREATE POLICY statements.
|
|
92
|
+
Schema for the expose section: https://run402.com/schemas/manifest.v1.json
|
|
82
93
|
|
|
83
|
-
⚠️ Without
|
|
84
|
-
|
|
94
|
+
⚠️ Without an "expose" entry, tables are unreachable via anon_key.
|
|
95
|
+
|
|
96
|
+
Legacy v1 bundle format (still accepted via compatibility shim):
|
|
97
|
+
Existing manifests with top-level "migrations" (string), "secrets" (array),
|
|
98
|
+
"functions" (array), "files" (array), "subdomain" (string), and the
|
|
99
|
+
"files[].file/data/path" + inline "manifest.json" entry continue to work —
|
|
100
|
+
the SDK translates them into a v2 ReleaseSpec under the hood. Prefer the
|
|
101
|
+
v2 shape above for new manifests; the legacy form is preserved for the
|
|
102
|
+
deprecation window so existing scripts don't break.
|
|
103
|
+
|
|
104
|
+
"migrations_file": "setup.sql" (legacy convenience) reads SQL from disk
|
|
105
|
+
relative to the manifest file. Useful when JSONB literals make inline
|
|
106
|
+
strings painful. Still supported on the legacy code path.
|
|
85
107
|
|
|
86
108
|
Examples:
|
|
87
109
|
run402 deploy --manifest app.json
|
|
88
110
|
run402 deploy --manifest app.json --project prj_123_1
|
|
89
111
|
cat app.json | run402 deploy
|
|
112
|
+
run402 deploy apply --manifest app.json # unified primitive (recommended)
|
|
90
113
|
|
|
91
114
|
Prerequisites:
|
|
92
115
|
- run402 init Set up allowance and funding
|
|
@@ -157,6 +180,50 @@ async function loadManifest(opts) {
|
|
|
157
180
|
});
|
|
158
181
|
}
|
|
159
182
|
|
|
183
|
+
// GH-185: Reject empty manifests client-side. Without this guard,
|
|
184
|
+
// `echo '{}' | run402 deploy` silently succeeds against the gateway with
|
|
185
|
+
// no signal that nothing was deployed. The MCP `deploy` tool was hardened
|
|
186
|
+
// for the same class of bug in #133; this is the CLI-side analog.
|
|
187
|
+
//
|
|
188
|
+
// "Meaningful" = at least one of these keys exists with non-empty content.
|
|
189
|
+
// We accept both shapes because this CLI path receives v1 manifests
|
|
190
|
+
// (translated by the bundleDeploy shim) and may also receive v2 manifests.
|
|
191
|
+
// v1: migrations, migrations_file, secrets, functions, files, subdomain
|
|
192
|
+
// v2: database, site, functions, secrets, subdomains, domains
|
|
193
|
+
// For object-typed v2 sections (site, database, functions, secrets,
|
|
194
|
+
// subdomains, domains) the "container is non-empty" check isn't enough —
|
|
195
|
+
// `site:{replace:{}}` has one key but ships nothing. We recurse one level
|
|
196
|
+
// so any object whose own values are all empty containers is still empty.
|
|
197
|
+
const meaningfulV1 = ["migrations", "migrations_file", "secrets", "functions", "files", "subdomain"];
|
|
198
|
+
const meaningfulV2 = ["database", "site", "functions", "secrets", "subdomains", "domains"];
|
|
199
|
+
const meaningful = [...new Set([...meaningfulV1, ...meaningfulV2])];
|
|
200
|
+
|
|
201
|
+
function hasContent(v) {
|
|
202
|
+
if (v == null) return false;
|
|
203
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
204
|
+
if (typeof v === "object") {
|
|
205
|
+
const keys = Object.keys(v);
|
|
206
|
+
if (keys.length === 0) return false;
|
|
207
|
+
return keys.some((k) => hasContent(v[k]));
|
|
208
|
+
}
|
|
209
|
+
if (typeof v === "string") return v.length > 0;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const hasMeaningfulContent = manifest && typeof manifest === "object" && !Array.isArray(manifest) && meaningful.some((key) => hasContent(manifest[key]));
|
|
214
|
+
if (!hasMeaningfulContent) {
|
|
215
|
+
fail({
|
|
216
|
+
code: "MANIFEST_EMPTY",
|
|
217
|
+
message: `Manifest contains no deployable sections. Expected at least one of: ${meaningful.join(", ")}`,
|
|
218
|
+
hint: "Did you mean to write a 'site.replace' or 'database.migrations' block? See https://run402.com/schemas/manifest.v1.json",
|
|
219
|
+
details: {
|
|
220
|
+
field: opts.manifest ? "manifest" : "stdin",
|
|
221
|
+
...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
|
|
222
|
+
meaningful_keys: meaningful,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
160
227
|
if (opts.manifest) {
|
|
161
228
|
try {
|
|
162
229
|
resolveMigrationsFile(manifest, baseDir);
|
package/lib/domains.mjs
CHANGED
|
@@ -9,7 +9,7 @@ Usage:
|
|
|
9
9
|
|
|
10
10
|
Subcommands:
|
|
11
11
|
add <domain> <subdomain_name> [--project <id>] Register a custom domain
|
|
12
|
-
list [<id>]
|
|
12
|
+
list [<id>] | list --project <id> List custom domains for a project
|
|
13
13
|
status <domain> [--project <id>] Check domain DNS/SSL status
|
|
14
14
|
delete <domain> --confirm [--project <id>] Release a custom domain. Requires --confirm.
|
|
15
15
|
|
|
@@ -26,6 +26,74 @@ Notes:
|
|
|
26
26
|
- The domain must CNAME to domains.run402.com (or ALIAS for apex domains)
|
|
27
27
|
`;
|
|
28
28
|
|
|
29
|
+
const SUB_HELP = {
|
|
30
|
+
add: `run402 domains add — Register a custom domain for a project
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
run402 domains add <domain> <subdomain_name> [--project <id>]
|
|
34
|
+
|
|
35
|
+
Arguments:
|
|
36
|
+
<domain> Custom domain (e.g. example.com)
|
|
37
|
+
<subdomain_name> Existing subdomain to map the custom domain to
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--project <id> Project ID (defaults to the active project)
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
- After adding, configure DNS as shown in the response
|
|
44
|
+
- Poll 'run402 domains status <domain>' until active
|
|
45
|
+
- The domain must CNAME to domains.run402.com (or ALIAS for apex domains)
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
run402 domains add example.com myapp
|
|
49
|
+
run402 domains add example.com myapp --project prj_abc123
|
|
50
|
+
`,
|
|
51
|
+
list: `run402 domains list — List custom domains for a project
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
run402 domains list [<id>]
|
|
55
|
+
|
|
56
|
+
Arguments:
|
|
57
|
+
<id> Project ID (defaults to the active project)
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
run402 domains list
|
|
61
|
+
run402 domains list prj_abc123
|
|
62
|
+
`,
|
|
63
|
+
status: `run402 domains status — Check DNS/SSL status of a custom domain
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
run402 domains status <domain> [--project <id>]
|
|
67
|
+
|
|
68
|
+
Arguments:
|
|
69
|
+
<domain> Custom domain to check
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--project <id> Project ID (defaults to the active project)
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
run402 domains status example.com
|
|
76
|
+
run402 domains status example.com --project prj_abc123
|
|
77
|
+
`,
|
|
78
|
+
delete: `run402 domains delete — Release a custom domain
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
run402 domains delete <domain> --confirm [--project <id>]
|
|
82
|
+
|
|
83
|
+
Arguments:
|
|
84
|
+
<domain> Custom domain to release
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
--confirm Required: releasing detaches the domain from this
|
|
88
|
+
project and clears its DNS/SSL configuration
|
|
89
|
+
(irreversible)
|
|
90
|
+
--project <id> Project ID (defaults to the active project)
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
run402 domains delete example.com --confirm
|
|
94
|
+
`,
|
|
95
|
+
};
|
|
96
|
+
|
|
29
97
|
function parseProjectFlag(args) {
|
|
30
98
|
let project = null;
|
|
31
99
|
const rest = [];
|
|
@@ -56,8 +124,14 @@ async function add(args) {
|
|
|
56
124
|
}
|
|
57
125
|
}
|
|
58
126
|
|
|
59
|
-
async function list(
|
|
60
|
-
const
|
|
127
|
+
async function list(args) {
|
|
128
|
+
const argList = Array.isArray(args) ? args : [];
|
|
129
|
+
const { project, rest } = parseProjectFlag(argList);
|
|
130
|
+
// Either --project <id> or a positional id is accepted; --project wins
|
|
131
|
+
// when both are supplied. Falls back to the active project when neither
|
|
132
|
+
// is given. Keeps backward-compat with the legacy `domains list <id>`
|
|
133
|
+
// form (GH-209).
|
|
134
|
+
const projectId = resolveProjectId(project || rest[0]);
|
|
61
135
|
try {
|
|
62
136
|
const data = await getSdk().domains.list(projectId);
|
|
63
137
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -113,10 +187,10 @@ async function deleteDomain(args) {
|
|
|
113
187
|
|
|
114
188
|
export async function run(sub, args) {
|
|
115
189
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
116
|
-
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(HELP); process.exit(0); }
|
|
190
|
+
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
|
|
117
191
|
switch (sub) {
|
|
118
192
|
case "add": await add(args); break;
|
|
119
|
-
case "list": await list(args
|
|
193
|
+
case "list": await list(args); break;
|
|
120
194
|
case "status": await status(args); break;
|
|
121
195
|
case "delete": await deleteDomain(args); break;
|
|
122
196
|
default:
|
package/lib/email.mjs
CHANGED
|
@@ -115,6 +115,40 @@ compatibility; new code should use 'info'.
|
|
|
115
115
|
|
|
116
116
|
Usage:
|
|
117
117
|
run402 email get-raw <message_id> [--output <file>] [--project <id>]
|
|
118
|
+
`,
|
|
119
|
+
create: `run402 email create — Create a project mailbox
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
run402 email create <slug> [--project <id>]
|
|
123
|
+
|
|
124
|
+
Arguments:
|
|
125
|
+
<slug> Mailbox slug (3-63 chars, lowercase alphanumeric +
|
|
126
|
+
hyphens, no consecutive hyphens). Becomes
|
|
127
|
+
<slug>@mail.run402.com.
|
|
128
|
+
|
|
129
|
+
Options:
|
|
130
|
+
--project <id> Project ID (defaults to the active project)
|
|
131
|
+
|
|
132
|
+
Notes:
|
|
133
|
+
- One mailbox per project
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
run402 email create my-app
|
|
137
|
+
run402 email create my-app --project prj_abc123
|
|
138
|
+
`,
|
|
139
|
+
get: `run402 email get — Get a message with replies
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
run402 email get <message_id> [--project <id>]
|
|
143
|
+
|
|
144
|
+
Arguments:
|
|
145
|
+
<message_id> Message ID to fetch
|
|
146
|
+
|
|
147
|
+
Options:
|
|
148
|
+
--project <id> Project ID (defaults to the active project)
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
run402 email get msg_abc123
|
|
118
152
|
`,
|
|
119
153
|
};
|
|
120
154
|
|