@odla-ai/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -57,6 +57,8 @@ __export(index_exports, {
57
57
  runCli: () => runCli,
58
58
  runHostedSecurity: () => runHostedSecurity,
59
59
  secretsPush: () => secretsPush,
60
+ secretsSet: () => secretsSet,
61
+ secretsSetClerkKey: () => secretsSetClerkKey,
60
62
  smoke: () => smoke,
61
63
  startHostedSecurityJob: () => startHostedSecurityJob
62
64
  });
@@ -67,7 +69,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
67
69
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
68
70
 
69
71
  // src/admin-ai.ts
70
- var import_node_process4 = __toESM(require("process"), 1);
72
+ var import_node_process5 = __toESM(require("process"), 1);
71
73
 
72
74
  // src/token.ts
73
75
  var import_db = require("@odla-ai/db");
@@ -297,9 +299,32 @@ function platformAudience(value) {
297
299
  return url.origin;
298
300
  }
299
301
 
302
+ // src/secret-input.ts
303
+ var import_node_process3 = __toESM(require("process"), 1);
304
+ var MAX_BYTES = 64 * 1024;
305
+ async function secretInputValue(options, kind = "credential") {
306
+ if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
307
+ let value;
308
+ if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
309
+ else if (options.stdin) value = await (options.readStdin ?? (() => readSecretStream(kind)))();
310
+ else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
311
+ value = value?.replace(/[\r\n]+$/, "");
312
+ if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : `stdin ${kind} is empty`);
313
+ if (new TextEncoder().encode(value).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
314
+ return value;
315
+ }
316
+ async function readSecretStream(kind, stream = import_node_process3.default.stdin) {
317
+ let value = "";
318
+ for await (const chunk of stream) {
319
+ value += String(chunk);
320
+ if (value.length > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
321
+ }
322
+ return value;
323
+ }
324
+
300
325
  // src/admin-ai-auth.ts
301
326
  var import_node_fs2 = require("fs");
302
- var import_node_process3 = __toESM(require("process"), 1);
327
+ var import_node_process4 = __toESM(require("process"), 1);
303
328
  var import_db2 = require("@odla-ai/db");
304
329
  async function getScopedPlatformToken(options) {
305
330
  return resolveAdminPlatformToken(options);
@@ -307,7 +332,7 @@ async function getScopedPlatformToken(options) {
307
332
  async function resolveAdminPlatformToken(options) {
308
333
  const audience = platformAudience(options.platform);
309
334
  if (options.token) return options.token;
310
- const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
335
+ const fromEnv = import_node_process4.default.env.ODLA_ADMIN_TOKEN;
311
336
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
312
337
  return scopedToken(
313
338
  audience,
@@ -319,7 +344,7 @@ async function resolveAdminPlatformToken(options) {
319
344
  }
320
345
  function audienceBoundEnvToken(token, platform) {
321
346
  const audience = platformAudience(platform);
322
- const declared = import_node_process3.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
347
+ const declared = import_node_process4.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
323
348
  if (declared) {
324
349
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
325
350
  } else if (audience !== "https://odla.ai") {
@@ -329,7 +354,7 @@ function audienceBoundEnvToken(token, platform) {
329
354
  }
330
355
  async function scopedToken(platform, scope, options, doFetch, out) {
331
356
  const audience = platformAudience(platform);
332
- const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
357
+ const tokenFile = options.tokenFile ?? `${import_node_process4.default.cwd()}/.odla/admin-token.local.json`;
333
358
  const cache = readJsonFile(tokenFile);
334
359
  const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
335
360
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -344,13 +369,13 @@ async function scopedToken(platform, scope, options, doFetch, out) {
344
369
  onCode: async ({ userCode, expiresIn }) => {
345
370
  const approvalUrl = handshakeUrl(audience, userCode);
346
371
  out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
347
- const shouldOpen = options.open === true || options.open !== false && !import_node_process3.default.env.CI && Boolean(import_node_process3.default.stdin.isTTY && import_node_process3.default.stdout.isTTY);
372
+ const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
348
373
  if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
349
374
  }
350
375
  });
351
376
  const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
352
377
  tokens[scope] = { token, expiresAt };
353
- if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
378
+ if ((0, import_node_fs2.existsSync)(`${import_node_process4.default.cwd()}/.git`)) ensureGitignore(import_node_process4.default.cwd(), [tokenFile]);
354
379
  writePrivateJson(tokenFile, { platform: audience, tokens });
355
380
  out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
356
381
  return token;
@@ -511,7 +536,7 @@ function isRecord2(value) {
511
536
 
512
537
  // src/admin-ai.ts
513
538
  async function adminAi(options) {
514
- const platform = platformAudience(options.platform ?? import_node_process4.default.env.ODLA_PLATFORM ?? "https://odla.ai");
539
+ const platform = platformAudience(options.platform ?? import_node_process5.default.env.ODLA_PLATFORM ?? "https://odla.ai");
515
540
  const doFetch = options.fetch ?? fetch;
516
541
  const out = options.stdout ?? console;
517
542
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -550,7 +575,7 @@ async function adminAi(options) {
550
575
  }
551
576
  if (options.action === "credential-set") {
552
577
  const provider = requireProvider(options.credentialProvider);
553
- const value = await credentialValue(options);
578
+ const value = await secretInputValue(options);
554
579
  const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
555
580
  method: "PUT",
556
581
  headers,
@@ -660,25 +685,6 @@ function requireProvider(value) {
660
685
  }
661
686
  return value;
662
687
  }
663
- async function credentialValue(options) {
664
- if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
665
- let value;
666
- if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
667
- else if (options.stdin) value = await (options.readStdin ?? readStdin)();
668
- else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
669
- value = value?.replace(/[\r\n]+$/, "");
670
- if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
671
- if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
672
- return value;
673
- }
674
- async function readStdin() {
675
- let value = "";
676
- for await (const chunk of import_node_process4.default.stdin) {
677
- value += String(chunk);
678
- if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
679
- }
680
- return value;
681
- }
682
688
  function policyArray(body) {
683
689
  if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
684
690
  if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
@@ -848,6 +854,7 @@ var CAPABILITIES = {
848
854
  "register the app and enable configured services per environment",
849
855
  "issue, persist, and explicitly rotate configured service credentials",
850
856
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
857
+ "store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
851
858
  "push db schema/rules and configure platform AI, auth, and deployment links",
852
859
  "validate config offline and smoke-test a provisioned db environment",
853
860
  "run app-attributed hosted security discovery and independent validation without provider keys",
@@ -1304,6 +1311,8 @@ Usage:
1304
1311
  odla-ai smoke [--config odla.config.mjs] [--env dev]
1305
1312
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1306
1313
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1314
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
1315
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
1307
1316
  odla-ai version
1308
1317
 
1309
1318
  Commands:
@@ -1317,7 +1326,9 @@ Commands:
1317
1326
  smoke Verify local credentials, public-config, live schema, and db aggregate.
1318
1327
  skill Same installer; --agent accepts all, claude, codex, cursor,
1319
1328
  copilot, gemini, or agents (repeatable or comma-separated).
1320
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1329
+ secrets Push configured db/o11y secrets into the Worker via wrangler
1330
+ stdin; set stores a tenant-vault secret and set-clerk-key the
1331
+ reserved Clerk secret key, write-only from stdin or an env var.
1321
1332
  version Print the CLI version.
1322
1333
 
1323
1334
  Safety:
@@ -1475,7 +1486,7 @@ function relativeDisplay(path, rootDir) {
1475
1486
  // src/provision.ts
1476
1487
  var import_apps2 = require("@odla-ai/apps");
1477
1488
  var import_ai = require("@odla-ai/ai");
1478
- var import_node_process5 = __toESM(require("process"), 1);
1489
+ var import_node_process6 = __toESM(require("process"), 1);
1479
1490
 
1480
1491
  // src/provision-credentials.ts
1481
1492
  var import_apps = require("@odla-ai/apps");
@@ -1779,7 +1790,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
1779
1790
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
1780
1791
  }
1781
1792
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
1782
- const key = import_node_process5.default.env[cfg.ai.keyEnv];
1793
+ const key = import_node_process6.default.env[cfg.ai.keyEnv];
1783
1794
  if (key) {
1784
1795
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
1785
1796
  await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -1839,6 +1850,63 @@ async function safeText2(res) {
1839
1850
  }
1840
1851
  }
1841
1852
 
1853
+ // src/secrets-set.ts
1854
+ var import_ai2 = require("@odla-ai/ai");
1855
+ var import_apps3 = require("@odla-ai/apps");
1856
+ var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
1857
+ async function secretsSet(options) {
1858
+ const name = (options.name ?? "").trim();
1859
+ if (!name) throw new Error('secret name is required, e.g. "odla-ai secrets set clerk_webhook_secret --env dev --stdin"');
1860
+ if (name.startsWith("$")) {
1861
+ throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
1862
+ }
1863
+ const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
1864
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
1865
+ try {
1866
+ await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
1867
+ } catch (err) {
1868
+ throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
1869
+ }
1870
+ out.log(`${name} stored in the ${tenantId} vault (write-only; the value was never echoed and cannot be read back here)`);
1871
+ }
1872
+ async function secretsSetClerkKey(options) {
1873
+ const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
1874
+ if (!value.startsWith("sk_")) throw new Error("the Clerk secret key must start with sk_ (sk_test_\u2026 or sk_live_\u2026)");
1875
+ if (value.startsWith("sk_test_") && PROD_ENV_NAMES2.has(options.env)) {
1876
+ throw new Error(`refusing to store an sk_test_ Clerk key for "${options.env}" \u2014 use the production instance's sk_live_ key`);
1877
+ }
1878
+ if (value.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
1879
+ throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
1880
+ }
1881
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
1882
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
1883
+ method: "POST",
1884
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1885
+ body: JSON.stringify({ value })
1886
+ });
1887
+ if (!res.ok) {
1888
+ const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value);
1889
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
1890
+ }
1891
+ out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
1892
+ }
1893
+ function scrubValue(text, value) {
1894
+ return redactSecrets(text).split(value).join("[value redacted]");
1895
+ }
1896
+ async function resolveVaultWrite(options) {
1897
+ const out = options.stdout ?? console;
1898
+ const doFetch = options.fetch ?? fetch;
1899
+ const cfg = await loadProjectConfig(options.configPath);
1900
+ if (!cfg.envs.includes(options.env)) {
1901
+ throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
1902
+ }
1903
+ if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
1904
+ throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
1905
+ }
1906
+ const value = await secretInputValue(options, "secret");
1907
+ return { cfg, tenantId: (0, import_apps3.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
1908
+ }
1909
+
1842
1910
  // src/security-command-context.ts
1843
1911
  var import_promises = require("readline/promises");
1844
1912
  async function hostedSecurityContext(parsed, dependencies) {
@@ -3180,8 +3248,26 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3180
3248
  }
3181
3249
  if (command === "secrets") {
3182
3250
  const sub = parsed.positionals[1];
3251
+ if (sub === "set" || sub === "set-clerk-key") {
3252
+ assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
3253
+ const options = {
3254
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3255
+ name: parsed.positionals[2],
3256
+ env: requiredString(parsed.options.env, "--env"),
3257
+ fromEnv: stringOpt(parsed.options["from-env"]),
3258
+ stdin: parsed.options.stdin === true,
3259
+ token: stringOpt(parsed.options.token),
3260
+ yes: parsed.options.yes === true,
3261
+ fetch: dependencies.fetch,
3262
+ stdout: dependencies.stdout
3263
+ };
3264
+ await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
3265
+ return;
3266
+ }
3183
3267
  if (sub !== "push") {
3184
- throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
3268
+ throw new Error(
3269
+ `unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev", "odla-ai secrets set <name> --env dev --stdin", or "odla-ai secrets set-clerk-key --env dev --stdin".`
3270
+ );
3185
3271
  }
3186
3272
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
3187
3273
  await secretsPush({
@@ -3250,6 +3336,8 @@ async function provisionCommand(parsed) {
3250
3336
  runCli,
3251
3337
  runHostedSecurity,
3252
3338
  secretsPush,
3339
+ secretsSet,
3340
+ secretsSetClerkKey,
3253
3341
  smoke,
3254
3342
  startHostedSecurityJob
3255
3343
  });