@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/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-BHAEDEL2.js";
4
+ } from "./chunk-MJYQ7YDH.js";
5
5
 
6
6
  // src/bin.ts
7
7
  runCli().catch((err) => {
@@ -297,7 +297,30 @@ function requireSystemAiPurpose(value) {
297
297
  }
298
298
 
299
299
  // src/admin-ai.ts
300
+ import process6 from "process";
301
+
302
+ // src/secret-input.ts
300
303
  import process5 from "process";
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 = process5.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 = process5.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
+ }
301
324
 
302
325
  // src/admin-ai-audit.ts
303
326
  function adminAiAuditQuery(filters) {
@@ -445,7 +468,7 @@ function isRecord2(value) {
445
468
 
446
469
  // src/admin-ai.ts
447
470
  async function adminAi(options) {
448
- const platform = platformAudience(options.platform ?? process5.env.ODLA_PLATFORM ?? "https://odla.ai");
471
+ const platform = platformAudience(options.platform ?? process6.env.ODLA_PLATFORM ?? "https://odla.ai");
449
472
  const doFetch = options.fetch ?? fetch;
450
473
  const out = options.stdout ?? console;
451
474
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -484,7 +507,7 @@ async function adminAi(options) {
484
507
  }
485
508
  if (options.action === "credential-set") {
486
509
  const provider = requireProvider(options.credentialProvider);
487
- const value = await credentialValue(options);
510
+ const value = await secretInputValue(options);
488
511
  const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
489
512
  method: "PUT",
490
513
  headers,
@@ -594,25 +617,6 @@ function requireProvider(value) {
594
617
  }
595
618
  return value;
596
619
  }
597
- async function credentialValue(options) {
598
- if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
599
- let value;
600
- if (options.fromEnv) value = process5.env[options.fromEnv];
601
- else if (options.stdin) value = await (options.readStdin ?? readStdin)();
602
- else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
603
- value = value?.replace(/[\r\n]+$/, "");
604
- if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
605
- if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
606
- return value;
607
- }
608
- async function readStdin() {
609
- let value = "";
610
- for await (const chunk of process5.stdin) {
611
- value += String(chunk);
612
- if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
613
- }
614
- return value;
615
- }
616
620
  function policyArray(body) {
617
621
  if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
618
622
  if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
@@ -648,6 +652,7 @@ var CAPABILITIES = {
648
652
  "register the app and enable configured services per environment",
649
653
  "issue, persist, and explicitly rotate configured service credentials",
650
654
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
655
+ "store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
651
656
  "push db schema/rules and configure platform AI, auth, and deployment links",
652
657
  "validate config offline and smoke-test a provisioned db environment",
653
658
  "run app-attributed hosted security discovery and independent validation without provider keys",
@@ -1259,7 +1264,7 @@ function assertWranglerConfig(cfg) {
1259
1264
  // src/provision.ts
1260
1265
  import { createAppsClient, tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
1261
1266
  import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
1262
- import process6 from "process";
1267
+ import process7 from "process";
1263
1268
 
1264
1269
  // src/provision-credentials.ts
1265
1270
  import { tenantIdFor } from "@odla-ai/apps";
@@ -1490,7 +1495,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
1490
1495
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
1491
1496
  }
1492
1497
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
1493
- const key = process6.env[cfg.ai.keyEnv];
1498
+ const key = process7.env[cfg.ai.keyEnv];
1494
1499
  if (key) {
1495
1500
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
1496
1501
  await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -1550,6 +1555,63 @@ async function safeText2(res) {
1550
1555
  }
1551
1556
  }
1552
1557
 
1558
+ // src/secrets-set.ts
1559
+ import { putSecret as putSecret2 } from "@odla-ai/ai";
1560
+ import { tenantIdFor as tenantIdFor3 } from "@odla-ai/apps";
1561
+ var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
1562
+ async function secretsSet(options) {
1563
+ const name = (options.name ?? "").trim();
1564
+ if (!name) throw new Error('secret name is required, e.g. "odla-ai secrets set clerk_webhook_secret --env dev --stdin"');
1565
+ if (name.startsWith("$")) {
1566
+ throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
1567
+ }
1568
+ const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
1569
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
1570
+ try {
1571
+ await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
1572
+ } catch (err) {
1573
+ throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
1574
+ }
1575
+ out.log(`${name} stored in the ${tenantId} vault (write-only; the value was never echoed and cannot be read back here)`);
1576
+ }
1577
+ async function secretsSetClerkKey(options) {
1578
+ const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
1579
+ if (!value.startsWith("sk_")) throw new Error("the Clerk secret key must start with sk_ (sk_test_\u2026 or sk_live_\u2026)");
1580
+ if (value.startsWith("sk_test_") && PROD_ENV_NAMES2.has(options.env)) {
1581
+ throw new Error(`refusing to store an sk_test_ Clerk key for "${options.env}" \u2014 use the production instance's sk_live_ key`);
1582
+ }
1583
+ if (value.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
1584
+ 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)`);
1585
+ }
1586
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
1587
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
1588
+ method: "POST",
1589
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1590
+ body: JSON.stringify({ value })
1591
+ });
1592
+ if (!res.ok) {
1593
+ const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value);
1594
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
1595
+ }
1596
+ out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
1597
+ }
1598
+ function scrubValue(text, value) {
1599
+ return redactSecrets(text).split(value).join("[value redacted]");
1600
+ }
1601
+ async function resolveVaultWrite(options) {
1602
+ const out = options.stdout ?? console;
1603
+ const doFetch = options.fetch ?? fetch;
1604
+ const cfg = await loadProjectConfig(options.configPath);
1605
+ if (!cfg.envs.includes(options.env)) {
1606
+ throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
1607
+ }
1608
+ if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
1609
+ throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
1610
+ }
1611
+ const value = await secretInputValue(options, "secret");
1612
+ return { cfg, tenantId: tenantIdFor3(cfg.app.id, options.env), value, doFetch, out };
1613
+ }
1614
+
1553
1615
  // src/security.ts
1554
1616
  import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve5, sep } from "path";
1555
1617
  import {
@@ -2542,6 +2604,8 @@ Usage:
2542
2604
  odla-ai smoke [--config odla.config.mjs] [--env dev]
2543
2605
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
2544
2606
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
2607
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
2608
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
2545
2609
  odla-ai version
2546
2610
 
2547
2611
  Commands:
@@ -2555,7 +2619,9 @@ Commands:
2555
2619
  smoke Verify local credentials, public-config, live schema, and db aggregate.
2556
2620
  skill Same installer; --agent accepts all, claude, codex, cursor,
2557
2621
  copilot, gemini, or agents (repeatable or comma-separated).
2558
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
2622
+ secrets Push configured db/o11y secrets into the Worker via wrangler
2623
+ stdin; set stores a tenant-vault secret and set-clerk-key the
2624
+ reserved Clerk secret key, write-only from stdin or an env var.
2559
2625
  version Print the CLI version.
2560
2626
 
2561
2627
  Safety:
@@ -3119,8 +3185,26 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3119
3185
  }
3120
3186
  if (command === "secrets") {
3121
3187
  const sub = parsed.positionals[1];
3188
+ if (sub === "set" || sub === "set-clerk-key") {
3189
+ assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
3190
+ const options = {
3191
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3192
+ name: parsed.positionals[2],
3193
+ env: requiredString(parsed.options.env, "--env"),
3194
+ fromEnv: stringOpt(parsed.options["from-env"]),
3195
+ stdin: parsed.options.stdin === true,
3196
+ token: stringOpt(parsed.options.token),
3197
+ yes: parsed.options.yes === true,
3198
+ fetch: dependencies.fetch,
3199
+ stdout: dependencies.stdout
3200
+ };
3201
+ await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
3202
+ return;
3203
+ }
3122
3204
  if (sub !== "push") {
3123
- throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
3205
+ throw new Error(
3206
+ `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".`
3207
+ );
3124
3208
  }
3125
3209
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
3126
3210
  await secretsPush({
@@ -3173,6 +3257,8 @@ export {
3173
3257
  initProject,
3174
3258
  secretsPush,
3175
3259
  provision,
3260
+ secretsSet,
3261
+ secretsSetClerkKey,
3176
3262
  runHostedSecurity,
3177
3263
  connectGitHubSecuritySource,
3178
3264
  listGitHubSecuritySources,
@@ -3192,4 +3278,4 @@ export {
3192
3278
  smoke,
3193
3279
  runCli
3194
3280
  };
3195
- //# sourceMappingURL=chunk-BHAEDEL2.js.map
3281
+ //# sourceMappingURL=chunk-MJYQ7YDH.js.map