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.
Files changed (51) hide show
  1. package/README.md +38 -0
  2. package/cli.mjs +7 -0
  3. package/core-dist/allowance-auth.js +42 -22
  4. package/lib/ci.mjs +395 -0
  5. package/lib/deploy-v2.mjs +200 -12
  6. package/lib/deploy.mjs +49 -7
  7. package/lib/sdk.mjs +2 -2
  8. package/lib/secrets.mjs +15 -7
  9. package/package.json +1 -1
  10. package/sdk/core-dist/allowance-auth.js +42 -22
  11. package/sdk/dist/ci-credentials.d.ts +22 -0
  12. package/sdk/dist/ci-credentials.d.ts.map +1 -0
  13. package/sdk/dist/ci-credentials.js +103 -0
  14. package/sdk/dist/ci-credentials.js.map +1 -0
  15. package/sdk/dist/index.d.ts +7 -1
  16. package/sdk/dist/index.d.ts.map +1 -1
  17. package/sdk/dist/index.js +5 -0
  18. package/sdk/dist/index.js.map +1 -1
  19. package/sdk/dist/namespaces/apps.d.ts +11 -4
  20. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  21. package/sdk/dist/namespaces/apps.js +73 -9
  22. package/sdk/dist/namespaces/apps.js.map +1 -1
  23. package/sdk/dist/namespaces/ci.d.ts +21 -0
  24. package/sdk/dist/namespaces/ci.d.ts.map +1 -0
  25. package/sdk/dist/namespaces/ci.js +256 -0
  26. package/sdk/dist/namespaces/ci.js.map +1 -0
  27. package/sdk/dist/namespaces/ci.types.d.ts +91 -0
  28. package/sdk/dist/namespaces/ci.types.d.ts.map +1 -0
  29. package/sdk/dist/namespaces/ci.types.js +8 -0
  30. package/sdk/dist/namespaces/ci.types.js.map +1 -0
  31. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  32. package/sdk/dist/namespaces/deploy.js +145 -30
  33. package/sdk/dist/namespaces/deploy.js.map +1 -1
  34. package/sdk/dist/namespaces/deploy.types.d.ts +24 -9
  35. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  36. package/sdk/dist/namespaces/secrets.d.ts +3 -2
  37. package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/secrets.js +45 -5
  39. package/sdk/dist/namespaces/secrets.js.map +1 -1
  40. package/sdk/dist/node/ci.d.ts +12 -0
  41. package/sdk/dist/node/ci.d.ts.map +1 -0
  42. package/sdk/dist/node/ci.js +30 -0
  43. package/sdk/dist/node/ci.js.map +1 -0
  44. package/sdk/dist/node/index.d.ts +7 -2
  45. package/sdk/dist/node/index.d.ts.map +1 -1
  46. package/sdk/dist/node/index.js +3 -2
  47. package/sdk/dist/node/index.js.map +1 -1
  48. package/sdk/dist/type-contract.d.ts +2 -0
  49. package/sdk/dist/type-contract.d.ts.map +1 -0
  50. package/sdk/dist/type-contract.js +2 -0
  51. 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": { "set": {...}, "delete": [...], "replace_all": {...} },
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": { "set": { "OPENAI_API_KEY": { "value": "sk-..." } } },
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
- if (!spec.project_id) spec.project_id = resolveProjectId(null);
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
- // Preserve the aggressive early exit when no allowance is configured.
226
- allowanceAuthHeaders("/deploy/v2/plans");
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
- reportSdkError(err);
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": { "set": { "OPENAI_API_KEY": { "value": "sk-..." } } },
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" 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).
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), "secrets" (array),
98
- "functions" (array), "files" (array), "subdomain" (string), and the
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
@@ -9,6 +9,6 @@
9
9
 
10
10
  import { run402 } from "#sdk/node";
11
11
 
12
- export function getSdk() {
13
- return run402();
12
+ export function getSdk(opts = {}) {
13
+ return run402(opts);
14
14
  }
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 sk-1234
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 with a value_hash (first 8 hex chars of SHA-256) for verifying the correct value was set
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' returns a value_hash for verification
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 sk-1234
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 with a value_hash (first 8 hex chars of SHA-256)
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
- console.log(JSON.stringify(data, null, 2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.54.4",
3
+ "version": "1.56.0",
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": {
@@ -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
- const message = formatSIWEMessage({
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
- signature,
129
- };
130
- return {
131
- "SIGN-IN-WITH-X": Buffer.from(JSON.stringify(payload)).toString("base64"),
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"}