run402 3.7.13 → 3.8.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 (97) hide show
  1. package/cli.mjs +6 -0
  2. package/core-dist/config.js +9 -0
  3. package/core-dist/keystore.js +103 -68
  4. package/core-dist/profile-state.js +137 -0
  5. package/lib/config.mjs +31 -20
  6. package/lib/credentials.mjs +273 -0
  7. package/lib/domains.mjs +40 -20
  8. package/lib/next-actions.mjs +7 -0
  9. package/lib/projects.mjs +65 -27
  10. package/lib/sdk-errors.mjs +24 -3
  11. package/lib/sdk.mjs +3 -2
  12. package/package.json +1 -1
  13. package/sdk/core-dist/config.js +9 -0
  14. package/sdk/core-dist/keystore.js +103 -68
  15. package/sdk/core-dist/profile-state.js +137 -0
  16. package/sdk/dist/credentials.d.ts +24 -7
  17. package/sdk/dist/credentials.d.ts.map +1 -1
  18. package/sdk/dist/credentials.js +4 -4
  19. package/sdk/dist/errors.d.ts +27 -1
  20. package/sdk/dist/errors.d.ts.map +1 -1
  21. package/sdk/dist/errors.js +61 -3
  22. package/sdk/dist/errors.js.map +1 -1
  23. package/sdk/dist/index.d.ts +7 -2
  24. package/sdk/dist/index.d.ts.map +1 -1
  25. package/sdk/dist/index.js +8 -3
  26. package/sdk/dist/index.js.map +1 -1
  27. package/sdk/dist/kernel.d.ts +2 -0
  28. package/sdk/dist/kernel.d.ts.map +1 -1
  29. package/sdk/dist/kernel.js +5 -1
  30. package/sdk/dist/kernel.js.map +1 -1
  31. package/sdk/dist/namespaces/ai.d.ts.map +1 -1
  32. package/sdk/dist/namespaces/ai.js +5 -10
  33. package/sdk/dist/namespaces/ai.js.map +1 -1
  34. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  35. package/sdk/dist/namespaces/apps.js +5 -13
  36. package/sdk/dist/namespaces/apps.js.map +1 -1
  37. package/sdk/dist/namespaces/assets.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/assets.js +9 -22
  39. package/sdk/dist/namespaces/assets.js.map +1 -1
  40. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  41. package/sdk/dist/namespaces/auth.js +16 -43
  42. package/sdk/dist/namespaces/auth.js.map +1 -1
  43. package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
  44. package/sdk/dist/namespaces/contracts.js +12 -31
  45. package/sdk/dist/namespaces/contracts.js.map +1 -1
  46. package/sdk/dist/namespaces/credentials.d.ts +48 -0
  47. package/sdk/dist/namespaces/credentials.d.ts.map +1 -0
  48. package/sdk/dist/namespaces/credentials.js +115 -0
  49. package/sdk/dist/namespaces/credentials.js.map +1 -0
  50. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  51. package/sdk/dist/namespaces/deploy.js +23 -3
  52. package/sdk/dist/namespaces/deploy.js.map +1 -1
  53. package/sdk/dist/namespaces/domains.d.ts +8 -2
  54. package/sdk/dist/namespaces/domains.d.ts.map +1 -1
  55. package/sdk/dist/namespaces/domains.js +41 -23
  56. package/sdk/dist/namespaces/domains.js.map +1 -1
  57. package/sdk/dist/namespaces/email.d.ts.map +1 -1
  58. package/sdk/dist/namespaces/email.js +8 -19
  59. package/sdk/dist/namespaces/email.js.map +1 -1
  60. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  61. package/sdk/dist/namespaces/functions.js +14 -37
  62. package/sdk/dist/namespaces/functions.js.map +1 -1
  63. package/sdk/dist/namespaces/jobs.d.ts.map +1 -1
  64. package/sdk/dist/namespaces/jobs.js +8 -19
  65. package/sdk/dist/namespaces/jobs.js.map +1 -1
  66. package/sdk/dist/namespaces/projects.d.ts +4 -4
  67. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  68. package/sdk/dist/namespaces/projects.js +18 -42
  69. package/sdk/dist/namespaces/projects.js.map +1 -1
  70. package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
  71. package/sdk/dist/namespaces/secrets.js +5 -10
  72. package/sdk/dist/namespaces/secrets.js.map +1 -1
  73. package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
  74. package/sdk/dist/namespaces/sender-domain.js +6 -16
  75. package/sdk/dist/namespaces/sender-domain.js.map +1 -1
  76. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  77. package/sdk/dist/namespaces/subdomains.js +5 -10
  78. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  79. package/sdk/dist/node/credentials.d.ts +18 -4
  80. package/sdk/dist/node/credentials.d.ts.map +1 -1
  81. package/sdk/dist/node/credentials.js +37 -9
  82. package/sdk/dist/node/credentials.js.map +1 -1
  83. package/sdk/dist/node/index.d.ts +4 -2
  84. package/sdk/dist/node/index.d.ts.map +1 -1
  85. package/sdk/dist/node/index.js +2 -1
  86. package/sdk/dist/node/index.js.map +1 -1
  87. package/sdk/dist/node/target-profile.d.ts.map +1 -1
  88. package/sdk/dist/node/target-profile.js +6 -4
  89. package/sdk/dist/node/target-profile.js.map +1 -1
  90. package/sdk/dist/project-auth-classification.d.ts +125 -0
  91. package/sdk/dist/project-auth-classification.d.ts.map +1 -0
  92. package/sdk/dist/project-auth-classification.js +135 -0
  93. package/sdk/dist/project-auth-classification.js.map +1 -0
  94. package/sdk/dist/project-credentials.d.ts +4 -0
  95. package/sdk/dist/project-credentials.d.ts.map +1 -0
  96. package/sdk/dist/project-credentials.js +9 -0
  97. package/sdk/dist/project-credentials.js.map +1 -0
@@ -0,0 +1,273 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import {
4
+ activeProfile,
5
+ getProject,
6
+ loadKeyStore,
7
+ projectCredentialsFile,
8
+ removeProject,
9
+ saveProject,
10
+ } from "./config.mjs";
11
+ import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
12
+ import { fail } from "./sdk-errors.mjs";
13
+
14
+ const HELP = `run402 credentials — Manage local credential material
15
+
16
+ Usage:
17
+ run402 credentials <subcommand> [args...]
18
+
19
+ Subcommands:
20
+ project-keys Manage the local project-key cache
21
+
22
+ Examples:
23
+ run402 credentials project-keys list
24
+ run402 credentials project-keys status --project prj_abc123
25
+ run402 credentials project-keys import --project prj_abc123 --service-key-stdin
26
+ run402 credentials project-keys export --project prj_abc123 --reveal
27
+ `;
28
+
29
+ const PROJECT_KEYS_HELP = `run402 credentials project-keys — Manage local project-key cache entries
30
+
31
+ Usage:
32
+ run402 credentials project-keys <subcommand> [options]
33
+
34
+ Subcommands:
35
+ list List cached project-key entries, redacted
36
+ status --project <id> Show one cached entry, redacted
37
+ import --project <id> --service-key-stdin
38
+ import --project <id> --service-key-env <env>
39
+ export --project <id> --reveal Print cached keys, including secrets
40
+ remove --project <id> Remove one cached key entry
41
+
42
+ Notes:
43
+ - This is a LOCAL CACHE surface. It is not project inventory.
44
+ - list/status never reveal full keys.
45
+ - export requires --reveal.
46
+ - import accepts service keys through stdin or an environment variable, not argv.
47
+ `;
48
+
49
+ function parseProjectKeyFlags(args, extraKnown = [], valueFlagsExtra = []) {
50
+ const parsed = normalizeArgv(args);
51
+ const valueFlags = ["--project", ...valueFlagsExtra];
52
+ assertKnownFlags(parsed, ["--project", "--help", "-h", ...extraKnown], valueFlags);
53
+ return {
54
+ projectId: flagValue(parsed, "--project"),
55
+ parsed,
56
+ rest: positionalArgs(parsed, valueFlags),
57
+ };
58
+ }
59
+
60
+ function fingerprint(value) {
61
+ if (!value) return null;
62
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
63
+ }
64
+
65
+ function prefix(value) {
66
+ if (!value) return null;
67
+ return `${value.slice(0, 8)}...`;
68
+ }
69
+
70
+ function provenance() {
71
+ const profile = activeProfile();
72
+ return {
73
+ source: "local_cache",
74
+ cache_path: projectCredentialsFile(),
75
+ wallet: profile,
76
+ profile,
77
+ };
78
+ }
79
+
80
+ function redactedEntry(projectId, entry) {
81
+ return {
82
+ project_id: projectId,
83
+ configured: Boolean(entry),
84
+ has_anon_key: Boolean(entry?.anon_key),
85
+ has_service_key: Boolean(entry?.service_key),
86
+ anon_key_prefix: prefix(entry?.anon_key),
87
+ service_key_prefix: prefix(entry?.service_key),
88
+ anon_key_fingerprint: fingerprint(entry?.anon_key),
89
+ service_key_fingerprint: fingerprint(entry?.service_key),
90
+ site_url: entry?.site_url ?? null,
91
+ cached_at: entry?.cached_at ?? null,
92
+ ...provenance(),
93
+ };
94
+ }
95
+
96
+ function requireProjectFlag(projectId, usage) {
97
+ if (!projectId) {
98
+ fail({
99
+ code: "BAD_USAGE",
100
+ message: "Missing --project <id>.",
101
+ hint: usage,
102
+ });
103
+ }
104
+ return projectId;
105
+ }
106
+
107
+ function requireCachedProject(projectId) {
108
+ const entry = getProject(projectId);
109
+ if (!entry) {
110
+ fail({
111
+ code: "PROJECT_CREDENTIAL_NOT_FOUND",
112
+ message: `No local project credentials cached for ${projectId}.`,
113
+ hint: "Import keys with `run402 credentials project-keys import --project <id> --service-key-stdin` if this operation truly requires local project credentials.",
114
+ details: { project_id: projectId, ...provenance() },
115
+ next_actions: [{
116
+ type: "run_command",
117
+ command: `run402 credentials project-keys import --project ${projectId} --service-key-stdin`,
118
+ why: "Import a service key for credential-required operations.",
119
+ }],
120
+ });
121
+ }
122
+ return entry;
123
+ }
124
+
125
+ async function list(args) {
126
+ const { rest } = parseProjectKeyFlags(args);
127
+ if (rest.length > 0) {
128
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for project-keys list: ${rest[0]}` });
129
+ }
130
+ const store = loadKeyStore(projectCredentialsFile());
131
+ const projects = Object.entries(store.projects ?? {})
132
+ .sort(([a], [b]) => a.localeCompare(b))
133
+ .map(([projectId, entry]) => redactedEntry(projectId, entry));
134
+ console.log(JSON.stringify({ projects, ...provenance() }, null, 2));
135
+ }
136
+
137
+ async function status(args) {
138
+ const { projectId, rest } = parseProjectKeyFlags(args);
139
+ if (rest.length > 0) {
140
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for project-keys status: ${rest[0]}` });
141
+ }
142
+ const id = requireProjectFlag(projectId, "run402 credentials project-keys status --project <id>");
143
+ console.log(JSON.stringify(redactedEntry(id, getProject(id)), null, 2));
144
+ }
145
+
146
+ function readSecretInput(parsed) {
147
+ const fromEnv = flagValue(parsed, "--service-key-env");
148
+ const fromStdin = parsed.includes("--service-key-stdin");
149
+ if (fromEnv && fromStdin) {
150
+ fail({ code: "BAD_USAGE", message: "Use either --service-key-env or --service-key-stdin, not both." });
151
+ }
152
+ if (fromEnv) {
153
+ const value = process.env[fromEnv];
154
+ if (!value) {
155
+ fail({
156
+ code: "BAD_ENV",
157
+ message: `Environment variable ${fromEnv} is empty or unset.`,
158
+ details: { env: fromEnv },
159
+ });
160
+ }
161
+ return value.trim();
162
+ }
163
+ if (fromStdin) return readFileSync(0, "utf-8").trim();
164
+ fail({
165
+ code: "BAD_USAGE",
166
+ message: "Import requires --service-key-stdin or --service-key-env <env>.",
167
+ hint: "Do not pass service keys as command-line values; argv can leak through shell history and process listings.",
168
+ });
169
+ }
170
+
171
+ async function importKey(args) {
172
+ const { projectId, parsed, rest } = parseProjectKeyFlags(
173
+ args,
174
+ ["--service-key-stdin", "--service-key-env", "--anon-key-env"],
175
+ ["--service-key-env", "--anon-key-env"],
176
+ );
177
+ if (rest.length > 0) {
178
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for project-keys import: ${rest[0]}` });
179
+ }
180
+ const id = requireProjectFlag(projectId, "run402 credentials project-keys import --project <id> --service-key-stdin");
181
+ const serviceKey = readSecretInput(parsed);
182
+ if (!serviceKey) {
183
+ fail({ code: "BAD_USAGE", message: "Service key input was empty." });
184
+ }
185
+ const anonEnv = flagValue(parsed, "--anon-key-env");
186
+ const anonKey = anonEnv ? process.env[anonEnv] : undefined;
187
+ if (anonEnv && !anonKey) {
188
+ fail({ code: "BAD_ENV", message: `Environment variable ${anonEnv} is empty or unset.`, details: { env: anonEnv } });
189
+ }
190
+ const existing = getProject(id);
191
+ saveProject(id, {
192
+ anon_key: anonKey ?? existing?.anon_key ?? "",
193
+ service_key: serviceKey,
194
+ site_url: existing?.site_url,
195
+ deployed_at: existing?.deployed_at,
196
+ last_deployment_id: existing?.last_deployment_id,
197
+ source: "manual_import",
198
+ cached_at: new Date().toISOString(),
199
+ });
200
+ console.log(JSON.stringify({ imported: true, ...redactedEntry(id, getProject(id)) }, null, 2));
201
+ }
202
+
203
+ async function exportKey(args) {
204
+ const { projectId, parsed, rest } = parseProjectKeyFlags(args, ["--reveal"]);
205
+ if (rest.length > 0) {
206
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for project-keys export: ${rest[0]}` });
207
+ }
208
+ const id = requireProjectFlag(projectId, "run402 credentials project-keys export --project <id> --reveal");
209
+ if (!parsed.includes("--reveal")) {
210
+ fail({
211
+ code: "REVEAL_REQUIRED",
212
+ message: "Exporting full project keys requires --reveal.",
213
+ hint: "Use `run402 credentials project-keys status --project <id>` for redacted output.",
214
+ details: { project_id: id, ...provenance() },
215
+ });
216
+ }
217
+ const entry = requireCachedProject(id);
218
+ console.log(JSON.stringify({ project_id: id, ...entry, ...provenance(), revealed: true }, null, 2));
219
+ }
220
+
221
+ async function remove(args) {
222
+ const { projectId, rest } = parseProjectKeyFlags(args);
223
+ if (rest.length > 0) {
224
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for project-keys remove: ${rest[0]}` });
225
+ }
226
+ const id = requireProjectFlag(projectId, "run402 credentials project-keys remove --project <id>");
227
+ const existed = Boolean(getProject(id));
228
+ removeProject(id, projectCredentialsFile());
229
+ console.log(JSON.stringify({ project_id: id, removed: existed, ...provenance() }, null, 2));
230
+ }
231
+
232
+ async function runProjectKeys(sub, args) {
233
+ if (!sub || sub === "--help" || sub === "-h") {
234
+ console.log(PROJECT_KEYS_HELP);
235
+ process.exit(0);
236
+ }
237
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
238
+ console.log(PROJECT_KEYS_HELP);
239
+ process.exit(0);
240
+ }
241
+ switch (sub) {
242
+ case "list": await list(args); break;
243
+ case "status": await status(args); break;
244
+ case "import": await importKey(args); break;
245
+ case "export": await exportKey(args); break;
246
+ case "remove": await remove(args); break;
247
+ default:
248
+ fail({
249
+ code: "UNKNOWN_SUBCOMMAND",
250
+ message: `Unknown credentials project-keys subcommand: ${sub}`,
251
+ hint: "Run `run402 credentials project-keys --help` for usage.",
252
+ details: { command: "credentials project-keys", subcommand: sub },
253
+ });
254
+ }
255
+ }
256
+
257
+ export async function run(sub, args = []) {
258
+ if (!sub || sub === "--help" || sub === "-h") {
259
+ console.log(HELP);
260
+ process.exit(0);
261
+ }
262
+ if (sub === "project-keys") {
263
+ const [projectKeySub, ...rest] = Array.isArray(args) ? args : [];
264
+ await runProjectKeys(projectKeySub, rest);
265
+ return;
266
+ }
267
+ fail({
268
+ code: "UNKNOWN_SUBCOMMAND",
269
+ message: `Unknown credentials subcommand: ${sub}`,
270
+ hint: "Run `run402 credentials --help` for usage.",
271
+ details: { command: "credentials", subcommand: sub },
272
+ });
273
+ }
package/lib/domains.mjs CHANGED
@@ -9,14 +9,15 @@ Usage:
9
9
  run402 domains <subcommand> [args...]
10
10
 
11
11
  Subcommands:
12
- add <domain> <subdomain_name> [--project <id>] Register a custom domain
13
- list [--project <id>] List custom domains for a project
14
- status <domain> [--project <id>] Check domain DNS/SSL status
15
- delete <domain> --confirm [--project <id>] Release a custom domain. Requires --confirm.
12
+ add <domain> <subdomain_name> [--project <id>] [--auth principal|service-key]
13
+ list [--project <id>] [--auth principal|service-key]
14
+ status <domain> [--project <id>] [--auth principal|service-key]
15
+ delete <domain> --confirm [--project <id>] [--auth principal|service-key]
16
16
 
17
17
  Examples:
18
18
  run402 domains add example.com myapp
19
- run402 domains add example.com myapp --project prj_123
19
+ run402 domains add example.com myapp --project prj_123
20
+ run402 domains list --project prj_123 --auth service-key
20
21
  run402 domains list
21
22
  run402 domains status example.com
22
23
  run402 domains delete example.com --confirm
@@ -31,7 +32,7 @@ const SUB_HELP = {
31
32
  add: `run402 domains add — Register a custom domain for a project
32
33
 
33
34
  Usage:
34
- run402 domains add <domain> <subdomain_name> [--project <id>]
35
+ run402 domains add <domain> <subdomain_name> [--project <id>] [--auth principal|service-key]
35
36
 
36
37
  Arguments:
37
38
  <domain> Custom domain (e.g. example.com)
@@ -39,6 +40,8 @@ Arguments:
39
40
 
40
41
  Options:
41
42
  --project <id> Project ID (defaults to the active project)
43
+ --auth <mode> principal (default, server-authoritative) or service-key
44
+ (uses local project-key cache)
42
45
 
43
46
  Notes:
44
47
  - After adding, configure DNS as shown in the response
@@ -52,10 +55,14 @@ Examples:
52
55
  list: `run402 domains list — List custom domains for a project
53
56
 
54
57
  Usage:
55
- run402 domains list [--project <id>]
58
+ run402 domains list [--project <id>] [--auth principal|service-key]
56
59
 
57
- Arguments:
58
- <id> Project ID (defaults to the active project)
60
+ Arguments:
61
+ <id> Project ID (defaults to the active project)
62
+
63
+ Options:
64
+ --auth <mode> principal (default, server-authoritative) or service-key
65
+ (uses local project-key cache)
59
66
 
60
67
  Examples:
61
68
  run402 domains list
@@ -64,13 +71,15 @@ Examples:
64
71
  status: `run402 domains status — Check DNS/SSL status of a custom domain
65
72
 
66
73
  Usage:
67
- run402 domains status <domain> [--project <id>]
74
+ run402 domains status <domain> [--project <id>] [--auth principal|service-key]
68
75
 
69
76
  Arguments:
70
77
  <domain> Custom domain to check
71
78
 
72
79
  Options:
73
80
  --project <id> Project ID (defaults to the active project)
81
+ --auth <mode> principal (default, server-authoritative) or service-key
82
+ (uses local project-key cache)
74
83
 
75
84
  Examples:
76
85
  run402 domains status example.com
@@ -79,7 +88,7 @@ Examples:
79
88
  delete: `run402 domains delete — Release a custom domain
80
89
 
81
90
  Usage:
82
- run402 domains delete <domain> --confirm [--project <id>]
91
+ run402 domains delete <domain> --confirm [--project <id>] [--auth principal|service-key]
83
92
 
84
93
  Arguments:
85
94
  <domain> Custom domain to release
@@ -89,6 +98,8 @@ Options:
89
98
  project and clears its DNS/SSL configuration
90
99
  (irreversible)
91
100
  --project <id> Project ID (defaults to the active project)
101
+ --auth <mode> principal (default, server-authoritative) or service-key
102
+ (uses local project-key cache)
92
103
 
93
104
  Examples:
94
105
  run402 domains delete example.com --confirm
@@ -97,17 +108,26 @@ Examples:
97
108
 
98
109
  function parseProjectFlag(args, extraKnown = []) {
99
110
  const parsedArgs = normalizeArgv(args);
100
- const valueFlags = ["--project"];
111
+ const valueFlags = ["--project", "--auth"];
101
112
  assertKnownFlags(parsedArgs, [...valueFlags, ...extraKnown, "--help", "-h"], valueFlags);
113
+ const auth = flagValue(parsedArgs, "--auth") ?? "principal";
114
+ if (auth !== "principal" && auth !== "service-key") {
115
+ fail({
116
+ code: "BAD_FLAG",
117
+ message: "--auth must be one of: principal, service-key.",
118
+ details: { flag: "--auth", value: auth, allowed: ["principal", "service-key"] },
119
+ });
120
+ }
102
121
  return {
103
122
  project: flagValue(parsedArgs, "--project"),
123
+ authMode: auth === "service-key" ? "service_key" : "principal",
104
124
  rest: positionalArgs(parsedArgs, valueFlags),
105
125
  args: parsedArgs,
106
126
  };
107
127
  }
108
128
 
109
129
  async function add(args) {
110
- const { project, rest } = parseProjectFlag(args);
130
+ const { project, authMode, rest } = parseProjectFlag(args);
111
131
  const domain = rest[0];
112
132
  const subdomainName = rest[1];
113
133
  if (!domain || !subdomainName) {
@@ -122,7 +142,7 @@ async function add(args) {
122
142
  }
123
143
  const projectId = resolveProjectId(project);
124
144
  try {
125
- const data = await getSdk().domains.add(projectId, { domain, subdomainName });
145
+ const data = await getSdk().domains.add(projectId, { domain, subdomainName, authMode });
126
146
  console.log(JSON.stringify(data, null, 2));
127
147
  } catch (err) {
128
148
  reportSdkError(err);
@@ -131,7 +151,7 @@ async function add(args) {
131
151
 
132
152
  async function list(args) {
133
153
  const argList = Array.isArray(args) ? args : [];
134
- const { project, rest } = parseProjectFlag(argList);
154
+ const { project, authMode, rest } = parseProjectFlag(argList);
135
155
  if (rest.length > 0) {
136
156
  fail({
137
157
  code: "BAD_USAGE",
@@ -141,7 +161,7 @@ async function list(args) {
141
161
  }
142
162
  const projectId = resolveProjectId(project);
143
163
  try {
144
- const data = await getSdk().domains.list(projectId);
164
+ const data = await getSdk().domains.list(projectId, { authMode });
145
165
  console.log(JSON.stringify(data, null, 2));
146
166
  } catch (err) {
147
167
  reportSdkError(err);
@@ -149,7 +169,7 @@ async function list(args) {
149
169
  }
150
170
 
151
171
  async function status(args) {
152
- const { project, rest } = parseProjectFlag(args);
172
+ const { project, authMode, rest } = parseProjectFlag(args);
153
173
  const domain = rest[0];
154
174
  if (!domain) {
155
175
  fail({
@@ -163,7 +183,7 @@ async function status(args) {
163
183
  }
164
184
  const projectId = resolveProjectId(project);
165
185
  try {
166
- const data = await getSdk().domains.status(projectId, domain);
186
+ const data = await getSdk().domains.status(projectId, domain, { authMode });
167
187
  console.log(JSON.stringify(data, null, 2));
168
188
  } catch (err) {
169
189
  reportSdkError(err);
@@ -171,7 +191,7 @@ async function status(args) {
171
191
  }
172
192
 
173
193
  async function deleteDomain(args) {
174
- const { project, rest, args: parsedArgs } = parseProjectFlag(args, ["--confirm"]);
194
+ const { project, authMode, rest, args: parsedArgs } = parseProjectFlag(args, ["--confirm"]);
175
195
  const domain = rest[0];
176
196
  if (!domain) {
177
197
  fail({
@@ -192,7 +212,7 @@ async function deleteDomain(args) {
192
212
  }
193
213
  const projectId = resolveProjectId(project);
194
214
  try {
195
- await getSdk().domains.remove(domain, { projectId });
215
+ await getSdk().domains.remove(domain, { projectId, authMode });
196
216
  console.log(JSON.stringify({ domain, project_id: projectId, released: true }));
197
217
  } catch (err) {
198
218
  reportSdkError(err);
@@ -46,6 +46,13 @@ export function createProjectAction() {
46
46
  });
47
47
  }
48
48
 
49
+ export function selectProjectAction() {
50
+ return nextAction("edit_request", {
51
+ command: "run402 projects use <project_id>",
52
+ why: "Select a server-visible project, or pass --project <project_id> on the command.",
53
+ });
54
+ }
55
+
49
56
  export function setTierAction(tier = "prototype") {
50
57
  return nextAction("renew_tier", {
51
58
  command: `run402 tier set ${tier}`,
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.