@moda-ai/cli 1.22.0 → 1.22.2

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 (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -777
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. package/dist/workspace.js +0 -1294
@@ -1,421 +0,0 @@
1
- // src/init/tenant.ts — Tenant selection and API key provisioning
2
- import { randomUUID } from 'node:crypto';
3
- import { existsSync, readFileSync, rmSync } from 'node:fs';
4
- import { hostname } from 'node:os';
5
- import { join } from 'node:path';
6
- import { select } from '@inquirer/prompts';
7
- import { authFetch, getCachedLoginContext } from '../auth.js';
8
- import { embeddedTenantIdFromApiKey, loadCliConfig, resolveIngestUrl, resolveModaBaseUrl, saveCliConfig, } from '../config.js';
9
- import { getModaDir, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, writeSecretJsonFile, } from '../profiles.js';
10
- import { CliAccessError, CliInputError, CliInputRequiredError } from '../errors.js';
11
- /**
12
- * Classify a failed /api/cli/context (or bootstrap) response so moda init
13
- * distinguishes retryable infra outages (Postgres pool exhaustion → 503
14
- * `database_unavailable`) from auth failures. Auth itself can succeed from
15
- * a cached moda_ct_ token even when this hydrate call 500s (MODA-742).
16
- */
17
- export function formatControlPlaneFetchError(action, status, body) {
18
- let parsed;
19
- try {
20
- parsed = JSON.parse(body);
21
- }
22
- catch {
23
- // Non-JSON body — fall through to generic formatting.
24
- }
25
- const isDbUnavailable = status === 503 &&
26
- (parsed?.error === 'database_unavailable' ||
27
- parsed?.retryable === true ||
28
- /database_unavailable|too many database connections|rds_reserved|connection pool/i.test(body));
29
- if (isDbUnavailable) {
30
- return (`Failed to ${action} (${status}): Moda control-plane database is temporarily unavailable ` +
31
- `(connection pool exhausted). This is not an authentication failure — your session token ` +
32
- `is fine. Retry in a few seconds.` +
33
- (parsed?.message ? ` (${parsed.message})` : ''));
34
- }
35
- if (status >= 500 &&
36
- /too many database connections|rds_reserved|remaining connection slots/i.test(body)) {
37
- return (`Failed to ${action} (${status}): Moda control-plane database ran out of connections. ` +
38
- `This is a retryable infra outage, not an auth failure. Retry in a few seconds. Body: ${body}`);
39
- }
40
- return `Failed to ${action} (${status}): ${body}`;
41
- }
42
- // Mirrors the backend's checkAdminAccess: only the viewer role is denied.
43
- const ADMIN_ACCESS_MESSAGE = 'You need admin or owner access to create API keys.';
44
- const ADMIN_ACCESS_GUIDANCE = 'Ask your organization owner to upgrade your role.';
45
- // Bound the pre-bootstrap liveness probe tightly: a slow or unreachable
46
- // endpoint must never block init — we just mint a fresh key instead.
47
- const VALIDATE_TIMEOUT_MS = 3_000;
48
- /**
49
- * Post-bootstrap validate-retry schedule (WP-2.3): key provisioning spans
50
- * Postgres + Cloudflare KV, so a freshly minted key can lag a moment before
51
- * it validates. Retry the liveness probe at these delays (3 attempts after
52
- * the initial call, ~3.5s of retry sleeps plus up to VALIDATE_TIMEOUT_MS per
53
- * probe against a hanging endpoint) before warning and moving on.
54
- */
55
- const POST_BOOTSTRAP_VALIDATE_DELAYS_MS = [
56
- 500, 1_000, 2_000,
57
- ];
58
- const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
59
- // The backend truncates the bootstrap label to 60 printable chars; clip the
60
- // hostname client-side too so what we send is what lands on the key name.
61
- const MAX_LABEL_LENGTH = 60;
62
- // Backend contract for the Idempotency-Key header (UUIDs match this too).
63
- const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
64
- /**
65
- * State file backing bootstrap idempotency across process restarts:
66
- * ~/.moda/pending-bootstrap.json maps tenant id -> the Idempotency-Key of an
67
- * in-flight (or failed) bootstrap POST. Written BEFORE the POST and cleared
68
- * on success, so a retry after a network failure reuses the same key and the
69
- * backend replays the original response instead of minting another ApiKey.
70
- *
71
- * Deliberately global (~/.moda, like config.json) and shared across
72
- * profiles: the server scopes keys to the authenticated user, so the worst
73
- * case of cross-profile sharing is one extra key minted — not worth
74
- * profile-scoping the file.
75
- */
76
- function pendingBootstrapPath(options = {}) {
77
- return join(getModaDir(options), 'pending-bootstrap.json');
78
- }
79
- function loadPendingBootstrapState(options = {}) {
80
- try {
81
- const path = pendingBootstrapPath(options);
82
- if (!existsSync(path))
83
- return {};
84
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
85
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
86
- return {};
87
- }
88
- const state = {};
89
- for (const [tenantId, entry] of Object.entries(parsed)) {
90
- if (entry &&
91
- typeof entry === 'object' &&
92
- typeof entry.idempotency_key === 'string') {
93
- state[tenantId] = entry;
94
- }
95
- }
96
- return state;
97
- }
98
- catch {
99
- // A corrupt state file must never block init; fresh keys are always safe.
100
- return {};
101
- }
102
- }
103
- /**
104
- * Return the Idempotency-Key to use for this tenant's bootstrap POST,
105
- * persisting it before the network call. A key left over from a previous
106
- * failed attempt is reused so the retry replays instead of double-minting.
107
- * File-system failures degrade to an in-memory key (init must never block).
108
- */
109
- function reserveBootstrapIdempotencyKey(tenantId, options = {}) {
110
- const state = loadPendingBootstrapState(options);
111
- const existing = state[tenantId]?.idempotency_key;
112
- if (existing && IDEMPOTENCY_KEY_PATTERN.test(existing)) {
113
- return existing;
114
- }
115
- const key = randomUUID();
116
- state[tenantId] = {
117
- idempotency_key: key,
118
- created_at: new Date().toISOString(),
119
- };
120
- try {
121
- writeSecretJsonFile(pendingBootstrapPath(options), state);
122
- }
123
- catch {
124
- // Best-effort persistence: without it a retry mints a fresh key, which
125
- // is the pre-idempotency behavior, not a failure.
126
- }
127
- return key;
128
- }
129
- /** Clear the tenant's pending key after a successful bootstrap. */
130
- function clearBootstrapIdempotencyKey(tenantId, options = {}) {
131
- try {
132
- const state = loadPendingBootstrapState(options);
133
- if (!(tenantId in state))
134
- return;
135
- delete state[tenantId];
136
- const path = pendingBootstrapPath(options);
137
- if (Object.keys(state).length === 0) {
138
- rmSync(path, { force: true });
139
- }
140
- else {
141
- writeSecretJsonFile(path, state);
142
- }
143
- }
144
- catch {
145
- // Leftover state is harmless: the key replays the stored response.
146
- }
147
- }
148
- /** Label sent in the bootstrap body; the backend sanitizes it further. */
149
- function bootstrapLabel(custom) {
150
- const clipped = custom?.trim().slice(0, MAX_LABEL_LENGTH);
151
- if (clipped)
152
- return clipped;
153
- try {
154
- return hostname().slice(0, MAX_LABEL_LENGTH) || 'moda init';
155
- }
156
- catch {
157
- return 'moda init';
158
- }
159
- }
160
- /**
161
- * Signed CLI keys embed their tenant (moda_sk_<base64url("v1:keyId:tenantId:
162
- * timestamp:signature")>). Decoding locally lets dedup reject a cross-tenant
163
- * or malformed key structurally, with no network call. Shared with the
164
- * pre-upload identity banner via `embeddedTenantIdFromApiKey` in config.ts;
165
- * liveness (not revoked) is probed separately by isStoredKeyLive().
166
- */
167
- const embeddedTenantId = embeddedTenantIdFromApiKey;
168
- /**
169
- * Liveness probe for a stored key. Deliberately NOT the backend's public
170
- * POST /api/api-keys/validate: the CLI's resolved base URL is the FRONTEND
171
- * origin (moda.dev in prod, localhost:3000 in dev), which has no
172
- * /api/api-keys/validate route and no rewrite for it — only /api/v1/data/* is
173
- * proxied to the backend. Probing the Data API is the only path that reaches
174
- * key validation from here; revisit when a validate proxy is added. Only a
175
- * 200 with a JSON body counts as live; 401/403 (revoked/invalid), any other
176
- * status, a non-JSON 200 (e.g. a frontend error page), a network error, or a
177
- * timeout all return false so init falls through to minting — never block.
178
- * The key value must never appear in logs, errors, or telemetry.
179
- */
180
- async function isStoredKeyLive(key, options = {}) {
181
- try {
182
- // Resolve the base URL with the same profile/env context as the stored-key
183
- // lookup, or a valid key for a non-active profile (e.g. `init --profile
184
- // staging` while prod is active) is probed against the wrong origin, read
185
- // as dead, and re-minted on every run.
186
- const response = await fetch(`${resolveModaBaseUrl(options)}/api/v1/data/overview`, {
187
- method: 'GET',
188
- headers: { 'x-api-key': key },
189
- signal: AbortSignal.timeout(VALIDATE_TIMEOUT_MS),
190
- });
191
- if (response.status !== 200)
192
- return false;
193
- await response.json();
194
- return true;
195
- }
196
- catch {
197
- return false;
198
- }
199
- }
200
- /**
201
- * A CLI key previously stored for this tenant, if any: the legacy
202
- * ~/.moda/config.json first, then the active profile's key reference. Only a
203
- * key whose stored tenant_id matches the selected tenant is a candidate.
204
- */
205
- function findStoredKeyForTenant(tenantId, options = {}) {
206
- const legacy = loadCliConfig(options);
207
- if (legacy.api_key && legacy.tenant_id === tenantId) {
208
- return legacy.api_key;
209
- }
210
- // Honors options.profile when init was run with --profile; otherwise the
211
- // active profile / MODA_PROFILE resolution applies.
212
- if (resolveTenantIdFromActiveProfile(options) === tenantId) {
213
- const profileKey = resolveApiKeyFromActiveProfile(options);
214
- if (profileKey)
215
- return profileKey;
216
- }
217
- return undefined;
218
- }
219
- /**
220
- * Post-bootstrap verification (WP-2.3): confirm a freshly minted key is live
221
- * before declaring success, reusing the same Data API probe as dedup.
222
- * Provisioning spans Postgres + Cloudflare KV (eventual consistency), so
223
- * failures are retried at the POST_BOOTSTRAP_VALIDATE_DELAYS_MS schedule.
224
- * Exhaustion is a stderr warning, never an init failure: init's final doctor
225
- * step surfaces persistent problems, and blocking here past the retry
226
- * schedule would punish every user for a transient propagation delay.
227
- */
228
- async function verifyFreshKeyLive(key, probeOptions, overrides) {
229
- const delays = overrides?.delaysMs ?? POST_BOOTSTRAP_VALIDATE_DELAYS_MS;
230
- const sleep = overrides?.sleep ?? defaultSleep;
231
- if (await isStoredKeyLive(key, probeOptions))
232
- return;
233
- for (const delayMs of delays) {
234
- await sleep(delayMs);
235
- if (await isStoredKeyLive(key, probeOptions))
236
- return;
237
- }
238
- console.error(' Warning: API key created but not yet active; propagation can take a few seconds. moda doctor --online will confirm.');
239
- }
240
- export async function selectTenantAndCreateKey(options = {}) {
241
- const log = (message) => {
242
- if (!options.silent)
243
- console.log(message);
244
- };
245
- // Auth-session reads and stored-key lookups must honor init's --profile, or
246
- // a profile-scoped login (secrets/auth/<profile>.json) is invisible here and
247
- // init fails with "Not authenticated" despite a fresh token.
248
- const authOptions = {
249
- profile: options.profile,
250
- env: options.env,
251
- };
252
- // 1. Fetch CLI context. A PKCE login that just ran in this process already
253
- // returned the user + tenants in the exchange response; reuse it and skip
254
- // one /api/cli/context round-trip. The cache is profile-keyed (a login for
255
- // a different profile returns null here), and the fetch stays as the
256
- // fallback for cached-token runs (no login happened) and legacy logins.
257
- let ctx;
258
- const cachedContext = getCachedLoginContext(authOptions);
259
- if (cachedContext) {
260
- ctx = cachedContext;
261
- }
262
- else {
263
- const contextRes = await authFetch('/api/cli/context', {}, authOptions);
264
- if (!contextRes.ok) {
265
- const body = await contextRes.text();
266
- throw new Error(formatControlPlaneFetchError('fetch context', contextRes.status, body));
267
- }
268
- ctx = (await contextRes.json());
269
- }
270
- log(` Authenticated as ${ctx.user.email}`);
271
- // 2. Select tenant
272
- if (ctx.tenants.length === 0) {
273
- throw new CliAccessError('No organizations found. Create one at https://moda.dev');
274
- }
275
- let selected;
276
- if (options.tenantId !== undefined) {
277
- // Exact id match only (case-sensitive; never slug/name, never a silent
278
- // fallback to another tenant).
279
- const match = ctx.tenants.find((tenant) => tenant.id === options.tenantId);
280
- if (!match) {
281
- const valid = ctx.tenants
282
- .map((tenant) => ` ${tenant.id} ${tenant.name} (${tenant.role})`)
283
- .join('\n');
284
- throw new CliInputError(`No organization with id '${options.tenantId}'. Valid organizations:\n${valid}`, 'Re-run with --tenant-id=<id> using one of the ids listed above.');
285
- }
286
- selected = match;
287
- log(` Organization: ${selected.name}`);
288
- }
289
- else if (ctx.tenants.length === 1) {
290
- selected = ctx.tenants[0];
291
- log(` Organization: ${selected.name}`);
292
- }
293
- else if (options.nonInteractive) {
294
- throw new CliInputRequiredError('Multiple Moda organizations are available; choose one and re-run with --tenant-id.', {
295
- id: 'init_tenant_selection',
296
- question: 'Select organization',
297
- kind: 'select',
298
- choices: ctx.tenants.map((tenant) => ({
299
- value: tenant.id,
300
- label: `${tenant.name} (${tenant.role})`,
301
- resume_flags: ['--tenant-id', tenant.id],
302
- })),
303
- resume_command: options.resumeCommand,
304
- }, 'Re-run `moda init --tenant-id <tenant_id>`.');
305
- }
306
- else {
307
- selected = await select({
308
- message: 'Select organization',
309
- choices: ctx.tenants.map((tenant) => ({
310
- name: `${tenant.name} (${tenant.role})`,
311
- value: tenant,
312
- })),
313
- });
314
- }
315
- // Role pre-check: the backend's bootstrap endpoint 403s viewers, so fail
316
- // before creating anything. The 403 handler below stays as a backstop.
317
- if (selected.role.toLowerCase() === 'viewer') {
318
- throw new CliAccessError(ADMIN_ACCESS_MESSAGE, { guidance: ADMIN_ACCESS_GUIDANCE });
319
- }
320
- // 3. Reuse a previously minted key for this tenant when it structurally
321
- // belongs to it (embedded tenant id) AND still works against the Data API,
322
- // so re-running init does not accumulate one CLI key per run. A failed
323
- // decode, tenant mismatch, or probe failure falls through to minting.
324
- let result;
325
- let reused = false;
326
- const storedKey = findStoredKeyForTenant(selected.id, authOptions);
327
- if (storedKey &&
328
- embeddedTenantId(storedKey) === selected.id &&
329
- (await isStoredKeyLive(storedKey, authOptions))) {
330
- log(` Using existing CLI key for ${selected.name} (created earlier)`);
331
- // A pending idempotency key left by an earlier failed bootstrap is moot
332
- // once the stored key is reused; clear it so it does not linger.
333
- clearBootstrapIdempotencyKey(selected.id, authOptions);
334
- reused = true;
335
- result = {
336
- apiKey: storedKey,
337
- tenantId: selected.id,
338
- tenantName: selected.name,
339
- tenantSlug: selected.slug,
340
- };
341
- }
342
- // 4. Create API key via bootstrap endpoint. The Idempotency-Key is
343
- // persisted to ~/.moda/pending-bootstrap.json BEFORE the POST, so a retry
344
- // after a network failure reuses the same key and the backend replays the
345
- // original response instead of minting a second ApiKey.
346
- if (!result) {
347
- const idempotencyKey = reserveBootstrapIdempotencyKey(selected.id, authOptions);
348
- const bootstrapRes = await authFetch('/api/cli/bootstrap', {
349
- method: 'POST',
350
- headers: {
351
- 'Content-Type': 'application/json',
352
- 'Idempotency-Key': idempotencyKey,
353
- },
354
- body: JSON.stringify({
355
- tenantId: selected.id,
356
- label: bootstrapLabel(options.label),
357
- }),
358
- }, authOptions);
359
- if (bootstrapRes.status === 403) {
360
- throw new CliAccessError(ADMIN_ACCESS_MESSAGE, { guidance: ADMIN_ACCESS_GUIDANCE });
361
- }
362
- if (bootstrapRes.status === 409) {
363
- const body = await bootstrapRes.text();
364
- let parsed;
365
- try {
366
- parsed = JSON.parse(body);
367
- }
368
- catch {
369
- // Fall through to the generic error below.
370
- }
371
- if (parsed?.error === 'cli_key_cap_reached') {
372
- throw new CliAccessError(parsed.message ||
373
- 'This organization has reached its CLI API key limit.', {
374
- // Command-agnostic: this path is shared by `moda init` and
375
- // `moda provision`. Prefer asking an admin to raise the
376
- // per-tenant cap; revoke/reuse remain valid escape hatches.
377
- guidance: 'Ask your organization admin to raise the CLI key limit in the dashboard ' +
378
- '(Settings → API Keys → CLI key limit), or revoke unused "CLI - ..." keys, ' +
379
- 'or reuse the existing key already saved in ~/.moda/config.json from a previous ' +
380
- 'moda init / moda provision --save, then retry.',
381
- });
382
- }
383
- throw new Error(`Failed to create API key (409): ${body}`);
384
- }
385
- if (!bootstrapRes.ok) {
386
- const body = await bootstrapRes.text();
387
- throw new Error(formatControlPlaneFetchError('create API key', bootstrapRes.status, body));
388
- }
389
- result = (await bootstrapRes.json());
390
- clearBootstrapIdempotencyKey(selected.id, authOptions);
391
- // Fresh mints only: the reused-key path above already proved liveness
392
- // via the dedup probe, so re-validating there would be a wasted call.
393
- await verifyFreshKeyLive(result.apiKey, authOptions, options.validateRetry);
394
- }
395
- // Persist to ~/.moda/config.json (mode 0600) so the snapshot/session-end
396
- // hook can authenticate (api_key) and the local dev orchestrator can scope
397
- // ClickHouse (tenant_id) without the user exporting env vars into the shell
398
- // that launches Claude. Env still wins over the file at read time.
399
- //
400
- // Headless callers (persistConfig: false) skip this block entirely: they
401
- // own their credential storage, and the env-shadow warning below only
402
- // makes sense when a selection was actually saved.
403
- if (options.persistConfig !== false) {
404
- saveCliConfig({
405
- api_key: result.apiKey,
406
- tenant_id: result.tenantId,
407
- ingest_url: resolveIngestUrl(authOptions),
408
- }, authOptions);
409
- // resolveTenantId() prefers MODA_TENANT_ID over the config file, so a stale
410
- // env var silently shadows the selection we just saved. Warn unless the env
411
- // var was itself the source of the selection.
412
- const envTenantId = (options.env ?? process.env).MODA_TENANT_ID;
413
- if (envTenantId && options.tenantIdSource !== 'env' && envTenantId !== result.tenantId) {
414
- console.error(` Warning: MODA_TENANT_ID is set to a different organization id and will shadow the saved selection (${result.tenantId}) for later commands. Unset it or set it to the selected organization id.`);
415
- }
416
- log(' API key + tenant saved to ~/.moda/config.json\n');
417
- log(' (Set MODA_API_KEY / MODA_TENANT_ID to override for scripts/CI.)\n');
418
- }
419
- return { ...result, reused };
420
- }
421
- //# sourceMappingURL=tenant.js.map