run402 1.54.4 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/cli.mjs +7 -0
- package/core-dist/allowance-auth.js +42 -22
- package/lib/ci.mjs +395 -0
- package/lib/deploy-v2.mjs +200 -12
- package/lib/deploy.mjs +49 -7
- package/lib/sdk.mjs +2 -2
- package/lib/secrets.mjs +15 -7
- package/package.json +1 -1
- package/sdk/core-dist/allowance-auth.js +42 -22
- package/sdk/dist/ci-credentials.d.ts +22 -0
- package/sdk/dist/ci-credentials.d.ts.map +1 -0
- package/sdk/dist/ci-credentials.js +103 -0
- package/sdk/dist/ci-credentials.js.map +1 -0
- package/sdk/dist/index.d.ts +7 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +5 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/apps.d.ts +11 -4
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js +73 -9
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/namespaces/ci.d.ts +21 -0
- package/sdk/dist/namespaces/ci.d.ts.map +1 -0
- package/sdk/dist/namespaces/ci.js +256 -0
- package/sdk/dist/namespaces/ci.js.map +1 -0
- package/sdk/dist/namespaces/ci.types.d.ts +91 -0
- package/sdk/dist/namespaces/ci.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/ci.types.js +8 -0
- package/sdk/dist/namespaces/ci.types.js.map +1 -0
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +145 -30
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +24 -9
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.d.ts +3 -2
- package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
- package/sdk/dist/namespaces/secrets.js +45 -5
- package/sdk/dist/namespaces/secrets.js.map +1 -1
- package/sdk/dist/node/ci.d.ts +12 -0
- package/sdk/dist/node/ci.d.ts.map +1 -0
- package/sdk/dist/node/ci.js +30 -0
- package/sdk/dist/node/ci.js.map +1 -0
- package/sdk/dist/node/index.d.ts +7 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +3 -2
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/type-contract.d.ts +2 -0
- package/sdk/dist/type-contract.d.ts.map +1 -0
- package/sdk/dist/type-contract.js +2 -0
- package/sdk/dist/type-contract.js.map +1 -0
package/lib/deploy-v2.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* "project_id": "...",
|
|
12
12
|
* "base": { "release": "current" } | { "release": "empty" } | { "release_id": "..." },
|
|
13
13
|
* "database": { "migrations": [...], "expose": {...}, "zero_downtime": false },
|
|
14
|
-
* "secrets": { "
|
|
14
|
+
* "secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] },
|
|
15
15
|
* "functions": { "replace": {...}, "patch": { "set": {...}, "delete": [...] } },
|
|
16
16
|
* "site": { "replace": {...} } | { "patch": { "put": {...}, "delete": [...] } },
|
|
17
17
|
* "subdomains": { "set": ["..."], "add": [...], "remove": [...] },
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
|
|
26
26
|
import { readFileSync } from "node:fs";
|
|
27
27
|
import { resolve, dirname, isAbsolute, join } from "node:path";
|
|
28
|
+
import { githubActionsCredentials } from "#sdk/node";
|
|
28
29
|
import { getSdk } from "./sdk.mjs";
|
|
29
30
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
30
|
-
import { allowanceAuthHeaders, resolveProjectId } from "./config.mjs";
|
|
31
|
+
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from "./config.mjs";
|
|
31
32
|
|
|
32
33
|
const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
|
|
33
34
|
|
|
34
35
|
Usage:
|
|
35
|
-
run402 deploy apply --manifest <path> [--project <id>] [--quiet]
|
|
36
|
-
run402 deploy apply --spec '<json>' [--project <id>] [--quiet]
|
|
36
|
+
run402 deploy apply --manifest <path> [--project <id>] [--quiet] [--allow-warnings]
|
|
37
|
+
run402 deploy apply --spec '<json>' [--project <id>] [--quiet] [--allow-warnings]
|
|
37
38
|
cat spec.json | run402 deploy apply [--project <id>]
|
|
38
39
|
|
|
39
40
|
Manifest format mirrors the MCP \`deploy\` tool's ReleaseSpec:
|
|
@@ -41,7 +42,7 @@ Manifest format mirrors the MCP \`deploy\` tool's ReleaseSpec:
|
|
|
41
42
|
"project_id": "prj_...",
|
|
42
43
|
"base": { "release": "current" },
|
|
43
44
|
"database": { "migrations": [{ "id": "001_init", "sql": "CREATE TABLE ..." }], "expose": {...} },
|
|
44
|
-
"secrets": { "
|
|
45
|
+
"secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] },
|
|
45
46
|
"functions": { "replace": { "api": { "source": { "data": "export default ..." } } } },
|
|
46
47
|
"site": { "replace": { "index.html": { "data": "<html>..." } } },
|
|
47
48
|
"subdomains": { "set": ["my-app"] }
|
|
@@ -52,11 +53,18 @@ Options:
|
|
|
52
53
|
--spec '<json>' Inline JSON spec (single-quote in shell)
|
|
53
54
|
--project <id> Override project_id from the manifest
|
|
54
55
|
--quiet Suppress per-event JSON-line stderr (final result still on stdout)
|
|
56
|
+
--allow-warnings Continue past plan warnings that require confirmation
|
|
55
57
|
|
|
56
58
|
Output:
|
|
57
|
-
stdout: { "status": "ok", "release_id": "rel_...", "operation_id": "op_...", "urls": {...} }
|
|
59
|
+
stdout: { "status": "ok", "release_id": "rel_...", "operation_id": "op_...", "urls": {...}, "warnings": [...] }
|
|
58
60
|
stderr: one JSON event per line (suppressed with --quiet)
|
|
59
61
|
|
|
62
|
+
Secrets:
|
|
63
|
+
Secret values do not belong in deploy manifests. Set them first:
|
|
64
|
+
run402 secrets set prj_... OPENAI_API_KEY --file ./.secrets/openai-key
|
|
65
|
+
Then deploy a value-free declaration:
|
|
66
|
+
{ "project_id": "prj_...", "secrets": { "require": ["OPENAI_API_KEY"] } }
|
|
67
|
+
|
|
60
68
|
Patch examples (only the listed file changes):
|
|
61
69
|
{ "project_id": "prj_...", "site": { "patch": { "put": { "index.html": { "data": "..." } } } } }
|
|
62
70
|
{ "project_id": "prj_...", "site": { "patch": { "delete": ["old.html"] } } }
|
|
@@ -128,13 +136,14 @@ function makeStderrEventWriter(quiet) {
|
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
async function applyCmd(args) {
|
|
131
|
-
const opts = { manifest: null, spec: null, project: null, quiet: false };
|
|
139
|
+
const opts = { manifest: null, spec: null, project: null, quiet: false, allowWarnings: false };
|
|
132
140
|
for (let i = 0; i < args.length; i++) {
|
|
133
141
|
if (args[i] === "--help" || args[i] === "-h") { console.log(APPLY_HELP); process.exit(0); }
|
|
134
142
|
if (args[i] === "--manifest" && args[i + 1]) { opts.manifest = args[++i]; continue; }
|
|
135
143
|
if (args[i] === "--spec" && args[i + 1]) { opts.spec = args[++i]; continue; }
|
|
136
144
|
if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
|
|
137
145
|
if (args[i] === "--quiet") { opts.quiet = true; continue; }
|
|
146
|
+
if (args[i] === "--allow-warnings") { opts.allowWarnings = true; continue; }
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
let raw;
|
|
@@ -165,6 +174,10 @@ async function applyCmd(args) {
|
|
|
165
174
|
details: { source: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin", parse_error: err.message },
|
|
166
175
|
});
|
|
167
176
|
}
|
|
177
|
+
rejectLegacySecretManifest(spec, {
|
|
178
|
+
source: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin",
|
|
179
|
+
...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
|
|
180
|
+
});
|
|
168
181
|
|
|
169
182
|
// GH-232: Reject empty specs client-side. Without this guard,
|
|
170
183
|
// `run402 deploy apply --spec '{}'` (and `--manifest <empty>`) would silently
|
|
@@ -213,7 +226,10 @@ async function applyCmd(args) {
|
|
|
213
226
|
});
|
|
214
227
|
}
|
|
215
228
|
if (opts.project) spec.project_id = opts.project;
|
|
216
|
-
|
|
229
|
+
const useGithubActionsOidc = hasGithubActionsOidcEnv();
|
|
230
|
+
if (!spec.project_id) {
|
|
231
|
+
spec.project_id = useGithubActionsOidc ? resolveCiProjectId() : resolveProjectId(null);
|
|
232
|
+
}
|
|
217
233
|
|
|
218
234
|
// Translate { project_id, ... } envelope → ReleaseSpec ({ project, ... })
|
|
219
235
|
// The SDK ReleaseSpec uses `project` rather than `project_id`; both shapes
|
|
@@ -222,17 +238,189 @@ async function applyCmd(args) {
|
|
|
222
238
|
const releaseSpec = mapManifestToReleaseSpec(spec);
|
|
223
239
|
const idempotencyKey = spec.idempotency_key;
|
|
224
240
|
|
|
225
|
-
|
|
226
|
-
|
|
241
|
+
let sdkOpts;
|
|
242
|
+
if (useGithubActionsOidc) {
|
|
243
|
+
sdkOpts = {
|
|
244
|
+
credentials: githubActionsCredentials({ projectId: spec.project_id, apiBase: API }),
|
|
245
|
+
disablePaidFetch: true,
|
|
246
|
+
};
|
|
247
|
+
} else {
|
|
248
|
+
// Preserve the aggressive early exit when no allowance is configured.
|
|
249
|
+
allowanceAuthHeaders("/deploy/v2/plans");
|
|
250
|
+
}
|
|
227
251
|
|
|
228
252
|
try {
|
|
229
|
-
const result = await getSdk().deploy.apply(releaseSpec, {
|
|
253
|
+
const result = await getSdk(sdkOpts).deploy.apply(releaseSpec, {
|
|
230
254
|
onEvent: makeStderrEventWriter(opts.quiet),
|
|
231
255
|
idempotencyKey,
|
|
256
|
+
allowWarnings: opts.allowWarnings,
|
|
232
257
|
});
|
|
233
258
|
console.log(JSON.stringify({ status: "ok", ...result }, null, 2));
|
|
234
259
|
} catch (err) {
|
|
235
|
-
|
|
260
|
+
reportDeployApplyError(err, useGithubActionsOidc);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function hasGithubActionsOidcEnv(env = process.env) {
|
|
265
|
+
return env.GITHUB_ACTIONS === "true" &&
|
|
266
|
+
Boolean(env.ACTIONS_ID_TOKEN_REQUEST_URL) &&
|
|
267
|
+
Boolean(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveCiProjectId(env = process.env) {
|
|
271
|
+
const projectId = getActiveProjectId() || env.RUN402_PROJECT_ID;
|
|
272
|
+
if (!projectId) {
|
|
273
|
+
fail({
|
|
274
|
+
code: "CI_PROJECT_REQUIRED",
|
|
275
|
+
message: "GitHub Actions OIDC deploy requires a project id.",
|
|
276
|
+
hint: "Pass --project <prj_...> in the workflow command, include project_id in the manifest, or set RUN402_PROJECT_ID.",
|
|
277
|
+
details: { sources: ["--project", "manifest.project_id", "active_project", "RUN402_PROJECT_ID"] },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return projectId;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const CI_DEPLOY_ERROR_GUIDANCE = {
|
|
284
|
+
invalid_token: {
|
|
285
|
+
hint: "Ensure the workflow has permissions: id-token: write and is running in the repository/branch linked with run402 ci link github.",
|
|
286
|
+
next_actions: [
|
|
287
|
+
"Check the workflow permissions block includes id-token: write.",
|
|
288
|
+
"Re-run run402 ci link github if the repository, branch, or environment changed.",
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
access_denied: {
|
|
292
|
+
hint: "The OIDC token was valid, but no active Run402 CI binding allowed this workflow.",
|
|
293
|
+
next_actions: [
|
|
294
|
+
"Run run402 ci list --project <prj_...> locally to inspect bindings.",
|
|
295
|
+
"Run run402 ci link github again for this repository/branch/environment.",
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
event_not_allowed: {
|
|
299
|
+
hint: "This binding only allows push and workflow_dispatch events in v1.",
|
|
300
|
+
next_actions: [
|
|
301
|
+
"Trigger the workflow with push or workflow_dispatch.",
|
|
302
|
+
"Create a separate follow-up design before enabling PR deploy events.",
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
repository_id_mismatch: {
|
|
306
|
+
hint: "The GitHub repository id in the OIDC token does not match the linked binding.",
|
|
307
|
+
next_actions: [
|
|
308
|
+
"Run run402 ci link github again from the current repository.",
|
|
309
|
+
"If automatic lookup fails, pass --repository-id with the numeric GitHub repository id.",
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
forbidden_spec_field: {
|
|
313
|
+
hint: "CI deploys in v1 can deploy site/functions/database content only; link locally for secrets, routes, subdomains, checks, or oversized manifests.",
|
|
314
|
+
next_actions: [
|
|
315
|
+
"Remove forbidden fields such as secrets, routes, subdomains, or checks from the CI manifest.",
|
|
316
|
+
"Keep the normalized manifest small enough to avoid manifest_ref.",
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
forbidden_plan: {
|
|
320
|
+
hint: "The gateway rejected this deploy plan for CI. Keep CI deploys to the v1 allowed resources and re-link if policy changed.",
|
|
321
|
+
next_actions: [
|
|
322
|
+
"Inspect the gateway error details for the rejected resource.",
|
|
323
|
+
"Run the deploy locally with run402 deploy apply for operations outside the CI allowlist.",
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
payment_required: {
|
|
327
|
+
hint: "The project tier or payment state does not allow this CI deploy.",
|
|
328
|
+
next_actions: [
|
|
329
|
+
"Run run402 tier status --project <prj_...> locally.",
|
|
330
|
+
"Renew or upgrade the project tier, then re-run the workflow.",
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
function reportDeployApplyError(err, useGithubActionsOidc) {
|
|
336
|
+
const warningEnhanced = enhanceDeployWarningError(err);
|
|
337
|
+
if (!useGithubActionsOidc) return reportSdkError(warningEnhanced);
|
|
338
|
+
return reportSdkError(enhanceCiDeployError(warningEnhanced));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function enhanceDeployWarningError(err) {
|
|
342
|
+
const existingBody = err?.body && typeof err.body === "object" && !Array.isArray(err.body)
|
|
343
|
+
? err.body
|
|
344
|
+
: {};
|
|
345
|
+
const warnings = Array.isArray(existingBody.warnings) ? existingBody.warnings : null;
|
|
346
|
+
const code = existingBody.code || err?.code || null;
|
|
347
|
+
if (!warnings && code !== "MISSING_REQUIRED_SECRET") return err;
|
|
348
|
+
|
|
349
|
+
const enhanced = Object.assign(new Error(err?.message || existingBody.message || String(code)), err);
|
|
350
|
+
const affected = warnings
|
|
351
|
+
? warnings.flatMap((w) => Array.isArray(w?.affected) ? w.affected : [])
|
|
352
|
+
: [];
|
|
353
|
+
enhanced.body = {
|
|
354
|
+
...existingBody,
|
|
355
|
+
code: code || "DEPLOY_WARNING_REQUIRES_CONFIRMATION",
|
|
356
|
+
message: existingBody.message || err?.message || "Deploy plan returned warnings that require confirmation.",
|
|
357
|
+
hint: existingBody.hint ||
|
|
358
|
+
(code === "MISSING_REQUIRED_SECRET"
|
|
359
|
+
? "Set the missing secret values with `run402 secrets set`, then retry deploy apply. Use --allow-warnings only after explicit review."
|
|
360
|
+
: "Review the plan warnings, then retry with --allow-warnings if you intentionally accept them."),
|
|
361
|
+
next_actions: Array.isArray(existingBody.next_actions) && existingBody.next_actions.length > 0
|
|
362
|
+
? existingBody.next_actions
|
|
363
|
+
: [
|
|
364
|
+
...(affected.length > 0
|
|
365
|
+
? [`Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`]
|
|
366
|
+
: []),
|
|
367
|
+
"Retry `run402 deploy apply` after resolving warnings.",
|
|
368
|
+
"Use `--allow-warnings` only when the warning was explicitly reviewed.",
|
|
369
|
+
],
|
|
370
|
+
...(warnings ? { warnings } : {}),
|
|
371
|
+
};
|
|
372
|
+
return enhanced;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function enhanceCiDeployError(err) {
|
|
376
|
+
const existingBody = err?.body && typeof err.body === "object" && !Array.isArray(err.body)
|
|
377
|
+
? err.body
|
|
378
|
+
: {};
|
|
379
|
+
const code = existingBody.code || err?.code || (err?.status === 402 ? "payment_required" : null);
|
|
380
|
+
const guidance = code ? CI_DEPLOY_ERROR_GUIDANCE[code] : null;
|
|
381
|
+
if (!guidance) return err;
|
|
382
|
+
|
|
383
|
+
const enhanced = Object.assign(new Error(err?.message || existingBody.message || String(code)), err);
|
|
384
|
+
enhanced.body = {
|
|
385
|
+
...existingBody,
|
|
386
|
+
code,
|
|
387
|
+
message: existingBody.message || err?.message || "GitHub Actions OIDC deploy failed.",
|
|
388
|
+
hint: existingBody.hint || guidance.hint,
|
|
389
|
+
next_actions: Array.isArray(existingBody.next_actions) && existingBody.next_actions.length > 0
|
|
390
|
+
? existingBody.next_actions
|
|
391
|
+
: guidance.next_actions,
|
|
392
|
+
};
|
|
393
|
+
return enhanced;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function rejectLegacySecretManifest(spec, details) {
|
|
397
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) return;
|
|
398
|
+
const secrets = spec.secrets;
|
|
399
|
+
if (secrets === undefined) return;
|
|
400
|
+
if (Array.isArray(secrets) && secrets.length > 0) {
|
|
401
|
+
fail({
|
|
402
|
+
code: "UNSAFE_SECRET_MANIFEST",
|
|
403
|
+
message: "Deploy manifests must not contain secret values. Legacy secrets arrays are no longer supported by deploy apply.",
|
|
404
|
+
hint: "Run `run402 secrets set <project> <KEY> --file <path>` first, then use `\"secrets\": { \"require\": [\"KEY\"] }` in the deploy manifest.",
|
|
405
|
+
details: { ...details, field: "secrets", legacy_shape: "array" },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (typeof secrets !== "object" || secrets === null) return;
|
|
409
|
+
if (Object.prototype.hasOwnProperty.call(secrets, "set")) {
|
|
410
|
+
fail({
|
|
411
|
+
code: "UNSAFE_SECRET_MANIFEST",
|
|
412
|
+
message: "Deploy manifests must not use secrets.set. Secret values are write-only and must be set outside deploy specs.",
|
|
413
|
+
hint: "Run `run402 secrets set <project> <KEY> --file <path>` first, then use `\"secrets\": { \"require\": [\"KEY\"] }`.",
|
|
414
|
+
details: { ...details, field: "secrets.set" },
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (Object.prototype.hasOwnProperty.call(secrets, "replace_all")) {
|
|
418
|
+
fail({
|
|
419
|
+
code: "UNSAFE_SECRET_MANIFEST",
|
|
420
|
+
message: "Deploy manifests must not use secrets.replace_all. Exact replacement is not representable in the value-free deploy contract.",
|
|
421
|
+
hint: "Use `secrets.require` for keys that must exist and `secrets.delete` for explicit removals.",
|
|
422
|
+
details: { ...details, field: "secrets.replace_all" },
|
|
423
|
+
});
|
|
236
424
|
}
|
|
237
425
|
}
|
|
238
426
|
|
package/lib/deploy.mjs
CHANGED
|
@@ -36,7 +36,7 @@ Manifest format (JSON, v2 ReleaseSpec — recommended):
|
|
|
36
36
|
]
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
-
"secrets": { "
|
|
39
|
+
"secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] },
|
|
40
40
|
"functions": {
|
|
41
41
|
"replace": {
|
|
42
42
|
"api": {
|
|
@@ -63,9 +63,13 @@ Manifest format (JSON, v2 ReleaseSpec — recommended):
|
|
|
63
63
|
Replace vs patch semantics per resource:
|
|
64
64
|
"site": { "replace": {...} } whole-site (omitted files removed)
|
|
65
65
|
"site": { "patch": { "put": {...}, "delete": [...] } } surgical updates
|
|
66
|
-
Same for "functions"
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
Same for "functions". Secrets are value-free declarations:
|
|
67
|
+
"secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] }
|
|
68
|
+
Secret values must be set outside deploy manifests with:
|
|
69
|
+
run402 secrets set prj_... OPENAI_API_KEY --file ./.secrets/openai-key
|
|
70
|
+
Migrations are always additive (each is keyed by id; re-shipping the same
|
|
71
|
+
id+sql is a registry noop, same id with different sql is a hard
|
|
72
|
+
MIGRATION_CHECKSUM_MISMATCH error).
|
|
69
73
|
|
|
70
74
|
File entries accept inline "data", a local "path", or a "sql_path"
|
|
71
75
|
(migrations only) — paths are resolved relative to the manifest file's
|
|
@@ -94,12 +98,14 @@ Manifest format (JSON, v2 ReleaseSpec — recommended):
|
|
|
94
98
|
⚠️ Without an "expose" entry, tables are unreachable via anon_key.
|
|
95
99
|
|
|
96
100
|
Legacy v1 bundle format (still accepted via compatibility shim):
|
|
97
|
-
Existing manifests with top-level "migrations" (string), "
|
|
98
|
-
"
|
|
101
|
+
Existing manifests with top-level "migrations" (string), "functions" (array),
|
|
102
|
+
"files" (array), "subdomain" (string), and the
|
|
99
103
|
"files[].file/data/path" + inline "manifest.json" entry continue to work —
|
|
100
104
|
the SDK translates them into a v2 ReleaseSpec under the hood. Prefer the
|
|
101
105
|
v2 shape above for new manifests; the legacy form is preserved for the
|
|
102
|
-
deprecation window so existing scripts don't break.
|
|
106
|
+
deprecation window so existing scripts don't break. Legacy file manifests
|
|
107
|
+
with secret values no longer deploy: run 'run402 secrets set' first, then
|
|
108
|
+
use 'run402 deploy apply' with 'secrets.require'.
|
|
103
109
|
|
|
104
110
|
"migrations_file": "setup.sql" (legacy convenience) reads SQL from disk
|
|
105
111
|
relative to the manifest file. Useful when JSONB literals make inline
|
|
@@ -224,6 +230,11 @@ async function loadManifest(opts) {
|
|
|
224
230
|
});
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
rejectUnsafeSecretManifest(manifest, {
|
|
234
|
+
source: opts.manifest ? "manifest" : "stdin",
|
|
235
|
+
...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
|
|
236
|
+
});
|
|
237
|
+
|
|
227
238
|
if (opts.manifest) {
|
|
228
239
|
try {
|
|
229
240
|
resolveMigrationsFile(manifest, baseDir);
|
|
@@ -255,6 +266,37 @@ async function loadManifest(opts) {
|
|
|
255
266
|
return manifest;
|
|
256
267
|
}
|
|
257
268
|
|
|
269
|
+
function rejectUnsafeSecretManifest(manifest, details) {
|
|
270
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return;
|
|
271
|
+
const secrets = manifest.secrets;
|
|
272
|
+
if (secrets === undefined) return;
|
|
273
|
+
if (Array.isArray(secrets) && secrets.length > 0) {
|
|
274
|
+
fail({
|
|
275
|
+
code: "UNSAFE_SECRET_MANIFEST",
|
|
276
|
+
message: "Deploy manifests must not contain secret values. Legacy top-level secrets arrays are no longer supported.",
|
|
277
|
+
hint: "Run `run402 secrets set <project> <KEY> --file <path>` first, then use `run402 deploy apply` with `\"secrets\": { \"require\": [\"KEY\"] }`.",
|
|
278
|
+
details: { ...details, field: "secrets", legacy_shape: "array" },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (!secrets || typeof secrets !== "object" || Array.isArray(secrets)) return;
|
|
282
|
+
if (Object.prototype.hasOwnProperty.call(secrets, "set")) {
|
|
283
|
+
fail({
|
|
284
|
+
code: "UNSAFE_SECRET_MANIFEST",
|
|
285
|
+
message: "Deploy manifests must not use secrets.set. Secret values are write-only and must be set outside deploy specs.",
|
|
286
|
+
hint: "Run `run402 secrets set <project> <KEY> --file <path>` first, then deploy with `\"secrets\": { \"require\": [\"KEY\"] }`.",
|
|
287
|
+
details: { ...details, field: "secrets.set" },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (Object.prototype.hasOwnProperty.call(secrets, "replace_all")) {
|
|
291
|
+
fail({
|
|
292
|
+
code: "UNSAFE_SECRET_MANIFEST",
|
|
293
|
+
message: "Deploy manifests must not use secrets.replace_all. Exact replacement is not representable in the value-free deploy contract.",
|
|
294
|
+
hint: "Use `secrets.require` for keys that must exist and `secrets.delete` for explicit removals.",
|
|
295
|
+
details: { ...details, field: "secrets.replace_all" },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
258
300
|
export async function run(args) {
|
|
259
301
|
// Subcommand dispatch (v1.34+):
|
|
260
302
|
// run402 deploy apply ... → unified deploy primitive (deploy.apply)
|
package/lib/sdk.mjs
CHANGED
package/lib/secrets.mjs
CHANGED
|
@@ -14,14 +14,15 @@ Subcommands:
|
|
|
14
14
|
delete <id> <key> Delete a secret from a project
|
|
15
15
|
|
|
16
16
|
Examples:
|
|
17
|
-
run402 secrets set prj_abc123 STRIPE_KEY
|
|
17
|
+
run402 secrets set prj_abc123 STRIPE_KEY --file ./.secrets/stripe-key
|
|
18
18
|
run402 secrets set prj_abc123 TLS_CERT --file cert.pem
|
|
19
19
|
run402 secrets list prj_abc123
|
|
20
20
|
run402 secrets delete prj_abc123 STRIPE_KEY
|
|
21
21
|
|
|
22
22
|
Notes:
|
|
23
23
|
- Secrets are injected as process.env in serverless functions
|
|
24
|
-
- Values are write-only — list returns keys
|
|
24
|
+
- Values are write-only — list returns keys and timestamps only
|
|
25
|
+
- Deploy manifests should declare existing keys with secrets.require; never put values in deploy specs
|
|
25
26
|
`;
|
|
26
27
|
|
|
27
28
|
const SUB_HELP = {
|
|
@@ -41,10 +42,11 @@ Options:
|
|
|
41
42
|
|
|
42
43
|
Notes:
|
|
43
44
|
- Secrets are injected as process.env in serverless functions
|
|
44
|
-
- Values are write-only; 'list'
|
|
45
|
+
- Values are write-only; 'list' cannot verify values by hash
|
|
46
|
+
- Prefer --file for real secrets so values do not land in shell history
|
|
45
47
|
|
|
46
48
|
Examples:
|
|
47
|
-
run402 secrets set prj_abc123 STRIPE_KEY
|
|
49
|
+
run402 secrets set prj_abc123 STRIPE_KEY --file ./.secrets/stripe-key
|
|
48
50
|
run402 secrets set prj_abc123 TLS_CERT --file cert.pem
|
|
49
51
|
`,
|
|
50
52
|
list: `run402 secrets list — List all secrets for a project
|
|
@@ -56,8 +58,7 @@ Arguments:
|
|
|
56
58
|
<id> Project ID (from 'run402 projects list')
|
|
57
59
|
|
|
58
60
|
Notes:
|
|
59
|
-
- Returns secret keys
|
|
60
|
-
for verifying the correct value was set; raw values are write-only
|
|
61
|
+
- Returns secret keys and timestamps only; raw values and value-derived hashes are never returned
|
|
61
62
|
|
|
62
63
|
Examples:
|
|
63
64
|
run402 secrets list prj_abc123
|
|
@@ -103,7 +104,14 @@ async function set(projectId, key, args = []) {
|
|
|
103
104
|
async function list(projectId) {
|
|
104
105
|
try {
|
|
105
106
|
const data = await getSdk().secrets.list(projectId);
|
|
106
|
-
|
|
107
|
+
const sanitized = {
|
|
108
|
+
secrets: (data.secrets || []).map((s) => ({
|
|
109
|
+
key: s.key,
|
|
110
|
+
...(s.created_at ? { created_at: s.created_at } : {}),
|
|
111
|
+
...(s.updated_at ? { updated_at: s.updated_at } : {}),
|
|
112
|
+
})),
|
|
113
|
+
};
|
|
114
|
+
console.log(JSON.stringify(sanitized, null, 2));
|
|
107
115
|
} catch (err) {
|
|
108
116
|
reportSdkError(err);
|
|
109
117
|
}
|
package/package.json
CHANGED
|
@@ -70,16 +70,44 @@ export function formatSIWEMessage(opts, address) {
|
|
|
70
70
|
opts.statement,
|
|
71
71
|
"",
|
|
72
72
|
`URI: ${opts.uri}`,
|
|
73
|
-
`Version: ${opts.version}`,
|
|
74
|
-
`Chain ID: ${opts.chainId}`,
|
|
73
|
+
`Version: ${opts.version ?? "1"}`,
|
|
74
|
+
`Chain ID: ${messageChainId(opts.chainId)}`,
|
|
75
75
|
`Nonce: ${opts.nonce}`,
|
|
76
76
|
`Issued At: ${opts.issuedAt}`,
|
|
77
77
|
];
|
|
78
78
|
if (opts.expirationTime) {
|
|
79
79
|
lines.push(`Expiration Time: ${opts.expirationTime}`);
|
|
80
80
|
}
|
|
81
|
+
if (opts.resources && opts.resources.length > 0) {
|
|
82
|
+
lines.push("Resources:");
|
|
83
|
+
for (const resource of opts.resources)
|
|
84
|
+
lines.push(`- ${resource}`);
|
|
85
|
+
}
|
|
81
86
|
return lines.join("\n");
|
|
82
87
|
}
|
|
88
|
+
export function buildSIWxAuthHeaders(opts) {
|
|
89
|
+
const message = formatSIWEMessage(opts, opts.allowance.address);
|
|
90
|
+
const signature = personalSign(opts.allowance.privateKey, opts.allowance.address, message);
|
|
91
|
+
const payload = {
|
|
92
|
+
domain: opts.domain,
|
|
93
|
+
address: toChecksumAddress(opts.allowance.address),
|
|
94
|
+
statement: opts.statement,
|
|
95
|
+
uri: opts.uri,
|
|
96
|
+
version: opts.version ?? "1",
|
|
97
|
+
chainId: payloadChainId(opts.chainId),
|
|
98
|
+
type: opts.type ?? "eip191",
|
|
99
|
+
nonce: opts.nonce,
|
|
100
|
+
issuedAt: opts.issuedAt,
|
|
101
|
+
expirationTime: opts.expirationTime,
|
|
102
|
+
signature,
|
|
103
|
+
};
|
|
104
|
+
if (opts.resources !== undefined) {
|
|
105
|
+
payload.resources = opts.resources;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
"SIGN-IN-WITH-X": Buffer.from(JSON.stringify(payload)).toString("base64"),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
83
111
|
/**
|
|
84
112
|
* Get SIWX auth headers for the Run402 API.
|
|
85
113
|
* Returns null if no allowance is configured.
|
|
@@ -103,32 +131,24 @@ export function getAllowanceAuthHeaders(path, allowancePath) {
|
|
|
103
131
|
const now = new Date();
|
|
104
132
|
const issuedAt = now.toISOString();
|
|
105
133
|
const expirationTime = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
|
|
106
|
-
|
|
134
|
+
return buildSIWxAuthHeaders({
|
|
135
|
+
allowance,
|
|
107
136
|
domain,
|
|
108
137
|
uri,
|
|
109
138
|
statement: "Sign in to Run402",
|
|
110
|
-
version: "1",
|
|
111
|
-
chainId: 84532, // Base Sepolia
|
|
112
|
-
nonce,
|
|
113
|
-
issuedAt,
|
|
114
|
-
expirationTime,
|
|
115
|
-
}, allowance.address);
|
|
116
|
-
const signature = personalSign(allowance.privateKey, allowance.address, message);
|
|
117
|
-
const payload = {
|
|
118
|
-
domain,
|
|
119
|
-
address: toChecksumAddress(allowance.address),
|
|
120
|
-
statement: "Sign in to Run402",
|
|
121
|
-
uri,
|
|
122
|
-
version: "1",
|
|
123
139
|
chainId: "eip155:84532",
|
|
124
|
-
type: "eip191",
|
|
125
140
|
nonce,
|
|
126
141
|
issuedAt,
|
|
127
142
|
expirationTime,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function messageChainId(chainId) {
|
|
146
|
+
if (typeof chainId === "number")
|
|
147
|
+
return String(chainId);
|
|
148
|
+
const match = /^eip155:(\d+)$/.exec(chainId);
|
|
149
|
+
return match ? match[1] : chainId;
|
|
150
|
+
}
|
|
151
|
+
function payloadChainId(chainId) {
|
|
152
|
+
return typeof chainId === "number" ? `eip155:${chainId}` : chainId;
|
|
133
153
|
}
|
|
134
154
|
//# sourceMappingURL=allowance-auth.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** CI-session credential helpers for OIDC-backed deploy flows. */
|
|
2
|
+
import type { CredentialsProvider } from "./credentials.js";
|
|
3
|
+
export declare const CI_SESSION_CREDENTIALS: unique symbol;
|
|
4
|
+
export interface CiMarkedCredentialsProvider extends CredentialsProvider {
|
|
5
|
+
readonly [CI_SESSION_CREDENTIALS]: true;
|
|
6
|
+
}
|
|
7
|
+
export interface CreateCiSessionCredentialsOptions {
|
|
8
|
+
projectId: string;
|
|
9
|
+
accessToken?: string;
|
|
10
|
+
getAccessToken?: () => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
export interface GithubActionsCredentialsOptions {
|
|
13
|
+
projectId: string;
|
|
14
|
+
apiBase?: string;
|
|
15
|
+
audience?: string;
|
|
16
|
+
refreshBeforeSeconds?: number;
|
|
17
|
+
fetch?: typeof globalThis.fetch;
|
|
18
|
+
}
|
|
19
|
+
export declare function isCiSessionCredentials(credentials: CredentialsProvider): credentials is CiMarkedCredentialsProvider;
|
|
20
|
+
export declare function createCiSessionCredentials(opts: CreateCiSessionCredentialsOptions): CiMarkedCredentialsProvider;
|
|
21
|
+
export declare function githubActionsCredentials(opts: GithubActionsCredentialsOptions): CiMarkedCredentialsProvider;
|
|
22
|
+
//# sourceMappingURL=ci-credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-credentials.d.ts","sourceRoot":"","sources":["../src/ci-credentials.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AASzE,eAAO,MAAM,sBAAsB,eAAmD,CAAC;AAEvF,MAAM,WAAW,2BAA4B,SAAQ,mBAAmB;IACtE,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,mBAAmB,GAC/B,WAAW,IAAI,2BAA2B,CAE5C;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,iCAAiC,GACtC,2BAA2B,CAuC7B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,+BAA+B,GACpC,2BAA2B,CA4B7B"}
|