run402 3.7.14 → 3.8.1

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 (104) hide show
  1. package/README.md +4 -2
  2. package/cli.mjs +6 -0
  3. package/core-dist/config.js +9 -0
  4. package/core-dist/keystore.js +103 -68
  5. package/core-dist/profile-state.js +137 -0
  6. package/lib/config.mjs +31 -20
  7. package/lib/credentials.mjs +273 -0
  8. package/lib/deploy-v2.mjs +11 -0
  9. package/lib/doctor.mjs +18 -2
  10. package/lib/domains.mjs +40 -20
  11. package/lib/next-actions.mjs +7 -0
  12. package/lib/projects.mjs +65 -27
  13. package/lib/sdk-errors.mjs +24 -3
  14. package/lib/sdk.mjs +3 -2
  15. package/lib/up.mjs +19 -0
  16. package/lib/up.test.mjs +29 -0
  17. package/lib/update-check.mjs +722 -0
  18. package/lib/update-check.test.mjs +438 -0
  19. package/package.json +1 -1
  20. package/sdk/core-dist/config.js +9 -0
  21. package/sdk/core-dist/keystore.js +103 -68
  22. package/sdk/core-dist/profile-state.js +137 -0
  23. package/sdk/dist/credentials.d.ts +24 -7
  24. package/sdk/dist/credentials.d.ts.map +1 -1
  25. package/sdk/dist/credentials.js +4 -4
  26. package/sdk/dist/errors.d.ts +27 -1
  27. package/sdk/dist/errors.d.ts.map +1 -1
  28. package/sdk/dist/errors.js +61 -3
  29. package/sdk/dist/errors.js.map +1 -1
  30. package/sdk/dist/index.d.ts +16 -2
  31. package/sdk/dist/index.d.ts.map +1 -1
  32. package/sdk/dist/index.js +9 -3
  33. package/sdk/dist/index.js.map +1 -1
  34. package/sdk/dist/kernel.d.ts +9 -0
  35. package/sdk/dist/kernel.d.ts.map +1 -1
  36. package/sdk/dist/kernel.js +40 -1
  37. package/sdk/dist/kernel.js.map +1 -1
  38. package/sdk/dist/namespaces/ai.d.ts.map +1 -1
  39. package/sdk/dist/namespaces/ai.js +5 -10
  40. package/sdk/dist/namespaces/ai.js.map +1 -1
  41. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/apps.js +5 -13
  43. package/sdk/dist/namespaces/apps.js.map +1 -1
  44. package/sdk/dist/namespaces/assets.d.ts.map +1 -1
  45. package/sdk/dist/namespaces/assets.js +9 -22
  46. package/sdk/dist/namespaces/assets.js.map +1 -1
  47. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  48. package/sdk/dist/namespaces/auth.js +16 -43
  49. package/sdk/dist/namespaces/auth.js.map +1 -1
  50. package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
  51. package/sdk/dist/namespaces/contracts.js +12 -31
  52. package/sdk/dist/namespaces/contracts.js.map +1 -1
  53. package/sdk/dist/namespaces/credentials.d.ts +48 -0
  54. package/sdk/dist/namespaces/credentials.d.ts.map +1 -0
  55. package/sdk/dist/namespaces/credentials.js +115 -0
  56. package/sdk/dist/namespaces/credentials.js.map +1 -0
  57. package/sdk/dist/namespaces/domains.d.ts +8 -2
  58. package/sdk/dist/namespaces/domains.d.ts.map +1 -1
  59. package/sdk/dist/namespaces/domains.js +41 -23
  60. package/sdk/dist/namespaces/domains.js.map +1 -1
  61. package/sdk/dist/namespaces/email.d.ts.map +1 -1
  62. package/sdk/dist/namespaces/email.js +8 -19
  63. package/sdk/dist/namespaces/email.js.map +1 -1
  64. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  65. package/sdk/dist/namespaces/functions.js +14 -37
  66. package/sdk/dist/namespaces/functions.js.map +1 -1
  67. package/sdk/dist/namespaces/jobs.d.ts.map +1 -1
  68. package/sdk/dist/namespaces/jobs.js +8 -19
  69. package/sdk/dist/namespaces/jobs.js.map +1 -1
  70. package/sdk/dist/namespaces/projects.d.ts +4 -4
  71. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  72. package/sdk/dist/namespaces/projects.js +18 -42
  73. package/sdk/dist/namespaces/projects.js.map +1 -1
  74. package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
  75. package/sdk/dist/namespaces/secrets.js +5 -10
  76. package/sdk/dist/namespaces/secrets.js.map +1 -1
  77. package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
  78. package/sdk/dist/namespaces/sender-domain.js +6 -16
  79. package/sdk/dist/namespaces/sender-domain.js.map +1 -1
  80. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  81. package/sdk/dist/namespaces/subdomains.js +5 -10
  82. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  83. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  84. package/sdk/dist/node/actions-node.js +46 -15
  85. package/sdk/dist/node/actions-node.js.map +1 -1
  86. package/sdk/dist/node/credentials.d.ts +18 -4
  87. package/sdk/dist/node/credentials.d.ts.map +1 -1
  88. package/sdk/dist/node/credentials.js +37 -9
  89. package/sdk/dist/node/credentials.js.map +1 -1
  90. package/sdk/dist/node/index.d.ts +11 -2
  91. package/sdk/dist/node/index.d.ts.map +1 -1
  92. package/sdk/dist/node/index.js +26 -1
  93. package/sdk/dist/node/index.js.map +1 -1
  94. package/sdk/dist/node/target-profile.d.ts.map +1 -1
  95. package/sdk/dist/node/target-profile.js +6 -4
  96. package/sdk/dist/node/target-profile.js.map +1 -1
  97. package/sdk/dist/project-auth-classification.d.ts +125 -0
  98. package/sdk/dist/project-auth-classification.d.ts.map +1 -0
  99. package/sdk/dist/project-auth-classification.js +135 -0
  100. package/sdk/dist/project-auth-classification.js.map +1 -0
  101. package/sdk/dist/project-credentials.d.ts +4 -0
  102. package/sdk/dist/project-credentials.d.ts.map +1 -0
  103. package/sdk/dist/project-credentials.js +9 -0
  104. package/sdk/dist/project-credentials.js.map +1 -0
package/lib/projects.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId, isCoreApiTarget } from "./config.mjs";
2
+ import { allowanceAuthHeaders, resolveProjectId, getActiveProjectId, isCoreApiTarget } from "./config.mjs";
3
3
  import { loadLiveOperatorSession } from "../core-dist/operator-session.js";
4
4
  import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
5
5
  import { withAutoApprove } from "./operator.mjs";
@@ -19,8 +19,7 @@ Subcommands:
19
19
  list [--org <id>] [--all] List your projects from the server (name, site_url, custom domains, org_id, active marker)
20
20
  rename <id> --name <label> Rename a project (fix an auto-generated name)
21
21
  get [id] Authoritative server read: status, org, tier, active deploy, mailbox, usage vs limits (live; no keys)
22
- info [id] Show local project details: REST URL, keys (local keystore only)
23
- keys [id] Print anon_key and service_key as JSON (local keystore only)
22
+ current Show the active project pointer and validation status
24
23
  sql [id] "<query>" [--file <path>] [--params '<json>'] Run a SQL query (supports parameterized queries)
25
24
  rest [id] <table> [params] Query a table via the REST API (PostgREST)
26
25
  usage [id] Show compute/storage usage for a project
@@ -45,7 +44,7 @@ Examples:
45
44
  run402 projects list --all
46
45
  run402 projects rename prj_abc123 --name "My Site"
47
46
  run402 projects get prj_abc123
48
- run402 projects info prj_abc123
47
+ run402 projects current
49
48
  run402 projects sql prj_abc123 "SELECT * FROM users LIMIT 5"
50
49
  run402 projects sql prj_abc123 "SELECT * FROM users WHERE id = $1" --params '[42]'
51
50
  run402 projects sql prj_abc123 --file setup.sql
@@ -56,7 +55,6 @@ Examples:
56
55
  run402 projects validate-expose prj_abc123 --file manifest.json
57
56
  run402 projects apply-expose prj_abc123 --file manifest.json
58
57
  run402 projects get-expose prj_abc123
59
- run402 projects keys prj_abc123
60
58
  run402 projects delete prj_abc123 --confirm
61
59
 
62
60
  Global options (any command):
@@ -69,7 +67,9 @@ Notes:
69
67
  - Most commands that take <id> default to the active project when omitted
70
68
  (set it with 'run402 projects use <id>'). Project IDs start with 'prj_';
71
69
  any first positional that doesn't is treated as the next argument instead.
72
- - 'list' is a SERVER read, not the local keystore: it shows every project the
70
+ - 'list', 'get', and 'use' are SERVER-authoritative project identity flows.
71
+ Local project keys live under 'run402 credentials project-keys ...'.
72
+ - 'list' is a SERVER read, not the local key cache: it shows every project the
73
73
  active wallet can reach (membership-scoped), with name, site_url, custom
74
74
  domains, and org_id. '--org <id>' filters to one org; '--all' reads the
75
75
  cross-wallet inventory for every wallet controlling your operator email
@@ -129,7 +129,7 @@ Examples:
129
129
  run402 projects list --all
130
130
  run402 projects list --wallet work
131
131
  `,
132
- rename: `run402 projects rename — Rename a project
132
+ rename: `run402 projects rename — Rename a project
133
133
 
134
134
  Usage:
135
135
  run402 projects rename <id> --name <label>
@@ -147,6 +147,16 @@ Notes:
147
147
 
148
148
  Examples:
149
149
  run402 projects rename prj_abc123 --name "My Site"
150
+ `,
151
+ current: `run402 projects current — Inspect active project selection
152
+
153
+ Usage:
154
+ run402 projects current
155
+
156
+ Notes:
157
+ - The active project pointer is local non-secret profile state, not proof that
158
+ local project keys exist.
159
+ - The command attempts an authoritative server read and reports validation.
150
160
  `,
151
161
  provision: `run402 projects provision — Provision a new Postgres project
152
162
 
@@ -541,28 +551,55 @@ async function get(projectId) {
541
551
  }
542
552
  }
543
553
 
544
- async function info(projectId) {
554
+ async function current() {
555
+ const projectId = getActiveProjectId();
556
+ const out = {
557
+ active_project_id: projectId ?? null,
558
+ source: "profile_state",
559
+ validation: { checked: false, ok: false },
560
+ };
561
+ if (!projectId) {
562
+ console.log(JSON.stringify(out, null, 2));
563
+ return;
564
+ }
545
565
  try {
546
- const data = await getSdk().projects.info(projectId);
547
- console.log(JSON.stringify({
548
- project_id: projectId,
549
- rest_url: `${API}/rest/v1`,
550
- anon_key: data.anon_key,
551
- service_key: data.service_key,
552
- site_url: data.site_url || null,
553
- }, null, 2));
566
+ const detail = await getSdk().projects.get(projectId);
567
+ out.validation = { checked: true, ok: true };
568
+ out.project = detail;
554
569
  } catch (err) {
555
- reportSdkError(err);
556
- }
570
+ out.validation = {
571
+ checked: true,
572
+ ok: false,
573
+ code: err?.code ?? null,
574
+ message: err?.message ?? String(err),
575
+ };
576
+ }
577
+ console.log(JSON.stringify(out, null, 2));
557
578
  }
558
579
 
559
- async function keys(projectId) {
560
- try {
561
- const data = await getSdk().projects.keys(projectId);
562
- console.log(JSON.stringify({ project_id: projectId, anon_key: data.anon_key, service_key: data.service_key }, null, 2));
563
- } catch (err) {
564
- reportSdkError(err);
565
- }
580
+ function commandMoved(name) {
581
+ fail({
582
+ code: "COMMAND_MOVED",
583
+ message: `run402 projects ${name} moved to the explicit local credential-cache surface.`,
584
+ hint: "Use `run402 credentials project-keys status/export --project <id>`.",
585
+ details: {
586
+ old_command: `projects ${name}`,
587
+ new_command: name === "keys" ? "credentials project-keys export --reveal" : "credentials project-keys status",
588
+ source: "local_cache",
589
+ },
590
+ next_actions: [
591
+ {
592
+ type: "run_command",
593
+ command: "run402 credentials project-keys status --project <id>",
594
+ why: "Inspect cached key presence without revealing secrets.",
595
+ },
596
+ {
597
+ type: "run_command",
598
+ command: "run402 credentials project-keys export --project <id> --reveal",
599
+ why: "Reveal cached project keys only when you explicitly need secret material.",
600
+ },
601
+ ],
602
+ });
566
603
  }
567
604
 
568
605
  async function sqlCmd(projectId, args = []) {
@@ -794,10 +831,11 @@ export async function run(sub, args) {
794
831
  case "provision": await provision(args); break;
795
832
  case "use": await use(args[0]); break;
796
833
  case "list": await list(args); break;
834
+ case "current": await current(); break;
797
835
  case "rename": { const { projectId, rest } = resolvePositionalProject(args, { rejectBareFirst: true, valueFlags: FLAGS_BY_SUB.rename.values }); await rename(projectId, rest); break; }
798
836
  case "get": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await get(projectId); break; }
799
- case "info": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await info(projectId); break; }
800
- case "keys": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await keys(projectId); break; }
837
+ case "info": commandMoved("info"); break;
838
+ case "keys": commandMoved("keys"); break;
801
839
  case "sql": { const { projectId, rest } = resolvePositionalProject(args, { maxBarePositionals: 1, valueFlags: FLAGS_BY_SUB.sql.values, rejectBareFirstWhenFlagPresent: ["--file"] }); await sqlCmd(projectId, rest); break; }
802
840
  case "rest": { const { projectId, rest: restArgs } = resolvePositionalProject(args); await rest(projectId, restArgs[0], restArgs[1]); break; }
803
841
  case "usage": { const { projectId } = resolvePositionalProject(args, { rejectBareFirst: true }); await usage(projectId); break; }
@@ -4,9 +4,8 @@
4
4
  * Maps SDK `Run402Error` subclasses into the CLI's canonical error envelope:
5
5
  * `{status: "error", code, message, retryable, safe_to_retry, ...}` on stderr,
6
6
  * `process.exit(1)`. Preserves specific behaviors:
7
- * - `ProjectNotFound` → canonical envelope with `code: "PROJECT_NOT_FOUND"`
8
- * and `details.source: "local_registry"` so callers can distinguish the
9
- * local-registry miss from a gateway 404.
7
+ * - `ProjectCredentialNotFound` → preserves
8
+ * `code: "PROJECT_CREDENTIAL_NOT_FOUND"` and local-cache provenance.
10
9
  * - HTML / non-JSON error bodies → `body_preview` field (first 500 chars),
11
10
  * matching GH-84 behavior.
12
11
  * - Network errors → `{status: "error", message: "..."}`.
@@ -66,6 +65,18 @@ export function parseFlagJson(name, value) {
66
65
  }
67
66
 
68
67
  export function reportSdkError(err) {
68
+ if (err?.name === "ProjectCredentialNotFound" || err?.code === "PROJECT_CREDENTIAL_NOT_FOUND") {
69
+ fail({
70
+ code: "PROJECT_CREDENTIAL_NOT_FOUND",
71
+ message: err?.message || "Local project credentials are not cached.",
72
+ hint: "This is a local credential-cache miss, not proof that the project does not exist. Use a principal-auth command, or import keys with `run402 credentials project-keys import --project <id> --service-key-stdin` for credential-required operations.",
73
+ details: err?.details,
74
+ next_actions: err?.nextActions,
75
+ retryable: err?.retryable ?? false,
76
+ safe_to_retry: err?.safeToRetry ?? true,
77
+ });
78
+ }
79
+
69
80
  if (err?.name === "ProjectNotFound") {
70
81
  const id = err.projectId || "";
71
82
  const hint = id && !String(id).startsWith("prj_")
@@ -117,6 +128,16 @@ export function reportSdkError(err) {
117
128
  "Returned as 403 even when the project does not exist, so verify the project id too.";
118
129
  }
119
130
 
131
+ if (typeof payload.code === "string" && payload.code.startsWith("PROJECT_CREDENTIAL_") && payload.hint === undefined) {
132
+ if (payload.code === "PROJECT_CREDENTIAL_PROJECT_MISMATCH") {
133
+ payload.hint =
134
+ "The supplied service key belongs to a different project than the explicit project id. Select the matching project key, or omit service-key mode and use the default principal-auth path when the command supports it.";
135
+ } else if (payload.code === "PROJECT_CREDENTIAL_INVALID" || payload.code === "PROJECT_CREDENTIAL_EXPIRED") {
136
+ payload.hint =
137
+ "The project credential was present but rejected by the server. Re-import or rotate the project key, or use a principal-auth command when available.";
138
+ }
139
+ }
140
+
120
141
  // Keep `status: "error"` as the outer envelope even if the response body
121
142
  // happened to contain its own `status` field (e.g. `{"status":"degraded"}`
122
143
  // from /health 503 responses). Downstream scripts match on this sentinel.
package/lib/sdk.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { run402 } from "#sdk/node";
11
- import { allowanceFile, projectsFile } from "./config.mjs";
11
+ import { allowanceFile, profileStateFile, projectCredentialsFile } from "./config.mjs";
12
12
 
13
13
  export function getSdk(opts = {}) {
14
14
  // surface: "cli" opts the default credential resolution into `auto` — wallet
@@ -16,7 +16,8 @@ export function getSdk(opts = {}) {
16
16
  return run402({
17
17
  surface: "cli",
18
18
  allowancePath: allowanceFile(),
19
- keystorePath: projectsFile(),
19
+ keystorePath: projectCredentialsFile(),
20
+ profileStatePath: profileStateFile(),
20
21
  ...opts,
21
22
  });
22
23
  }
package/lib/up.mjs CHANGED
@@ -3,6 +3,7 @@ import { stdin as input, stderr as output } from "node:process";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail } from "./sdk-errors.mjs";
5
5
  import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
6
+ import { createUpdateCheckScheduler, emitUpdateNotice } from "./update-check.mjs";
6
7
 
7
8
  const HELP = `run402 up — Provision/link/deploy the current app
8
9
 
@@ -44,6 +45,11 @@ Options:
44
45
  --json-stream Emit NDJSON progress events on stdout and a final result event.
45
46
  --quiet Suppress action progress events on stderr.
46
47
 
48
+ Update notices:
49
+ Stale CLI notices are advisory and never change the result payload or exit
50
+ code. Non-streaming notices are JSON on stderr; --json-stream emits
51
+ cli.update_available as an NDJSON event.
52
+
47
53
  Project resolution:
48
54
  explicit --project > .run402/project.json > manifest project_id > approved
49
55
  project creation from --name > approved active-project fallback.
@@ -184,6 +190,10 @@ export async function run(args = []) {
184
190
  });
185
191
  }
186
192
  const allowWarningCodes = collectRepeatedValues(parsed, "--allow-warning");
193
+ const updateScheduler = createUpdateCheckScheduler({
194
+ command: ["run402", "up", ...parsed],
195
+ });
196
+ emitUpdateNotice(updateScheduler.cachedNotice, { jsonStream, quiet });
187
197
 
188
198
  try {
189
199
  const sdk = getSdk();
@@ -224,11 +234,20 @@ export async function run(args = []) {
224
234
  } else {
225
235
  console.log(JSON.stringify(result, null, 2));
226
236
  }
237
+ if (shouldExitNonZeroForUpResult(result)) {
238
+ process.exitCode = 1;
239
+ }
227
240
  } catch (err) {
228
241
  reportSdkError(err);
229
242
  }
230
243
  }
231
244
 
245
+ export function shouldExitNonZeroForUpResult(result) {
246
+ return result?.action === "up" &&
247
+ result?.mode === "apply" &&
248
+ result?.result?.app_result?.status === "deployed_unverified";
249
+ }
250
+
232
251
  function parseExecutionMode(args) {
233
252
  const modes = [];
234
253
  if (args.includes("--check")) modes.push("--check");
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { shouldExitNonZeroForUpResult } from "./up.mjs";
4
+
5
+ test("run402 up exits nonzero for deployed-but-unverified app results", () => {
6
+ assert.equal(shouldExitNonZeroForUpResult({
7
+ action: "up",
8
+ mode: "apply",
9
+ result: {
10
+ app_result: { status: "deployed_unverified" },
11
+ },
12
+ }), true);
13
+
14
+ assert.equal(shouldExitNonZeroForUpResult({
15
+ action: "up",
16
+ mode: "apply",
17
+ result: {
18
+ app_result: { status: "succeeded" },
19
+ },
20
+ }), false);
21
+
22
+ assert.equal(shouldExitNonZeroForUpResult({
23
+ action: "up",
24
+ mode: "check",
25
+ result: {
26
+ app_result: { status: "deployed_unverified" },
27
+ },
28
+ }), false);
29
+ });