@odla-ai/cli 0.29.0 → 0.30.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/README.md CHANGED
@@ -482,8 +482,8 @@ commands continue to default to the sandbox.
482
482
  `provision` performs the standard safe setup flow:
483
483
 
484
484
  When using `--push-secrets`, create the Worker and its Wrangler config first.
485
- The CLI checks that config and `wrangler whoami` before it issues or rotates a
486
- shown-once credential.
485
+ The CLI checks that config and `wrangler whoami` before it issues a shown-once
486
+ credential set.
487
487
 
488
488
  1. Gets an `odla_dev_...` token by email-bound device handshake, or reuses
489
489
  `ODLA_DEV_TOKEN` / `.odla/dev-token.json`. A fresh handshake requires
@@ -521,8 +521,9 @@ shown-once credential.
521
521
  reservation approved in step 1; it is not ambient project-creation authority.
522
522
  3. Enables configured services in every configured environment. Calendar
523
523
  config is normalized per env and fixed to Google read-only access.
524
- 4. Mints or reuses each env's configured service credentials: an odla-db app
525
- key for `db` and an ingest token for `o11y`.
524
+ 4. For local development, mints or reuses that developer's configured service
525
+ credentials. Multiple developers may hold independent DB/o11y credentials
526
+ for the same shared sandbox tenant.
526
527
  5. Collision-checks and merges declared integration schema/rule fragments,
527
528
  pushes the composed database contract, and creates integration seeds only
528
529
  when their natural-key rows are absent.
@@ -533,23 +534,40 @@ shown-once credential.
533
534
  exact Google OAuth endpoint (or a same-platform interstitial). The human completes consent there;
534
535
  provision follows the exact attempt through initial sync. OAuth codes and
535
536
  refresh tokens never enter the CLI, repo, chat, or app.
536
- 8. Writes `.odla/credentials.local.json` with mode `0600`.
537
+ 8. Local-only provisioning writes `.odla/credentials.local.json` with mode
538
+ `0600`. A deployment run with `--push-secrets` does not write runtime
539
+ credential plaintext there.
537
540
  9. With `--write-dev-vars`, writes the local Worker values to `.dev.vars`; with
538
- `--push-secrets`, transfers the configured service secrets to the matching
539
- deployed Worker environment over Wrangler stdin.
540
-
541
- Plain reruns are idempotent. Pass `--rotate-o11y-token` only when deliberately
542
- invalidating and replacing the o11y token; combine it with `--push-secrets` so
543
- the replacement reaches the Worker in the same run. `--rotate-keys` remains the
544
- broader credential rotation. Production mutation or secret transfer requires
545
- `--yes`. Pass `--no-write-credentials` to skip writing the local credentials
546
- file; it cannot be combined with an operation that would issue a shown-once
547
- credential.
548
-
549
- Rotation and Wrangler secret replacement are not a distributed transaction.
550
- The CLI preflights Wrangler first and persists the replacement before pushing;
551
- if that final push still fails, follow the printed `secrets push --env ...`
552
- retry. Do not run the rotation flag again.
541
+ `--push-secrets`, binds a fresh additive credential set to the exact
542
+ Cloudflare account/script/environment, transfers both values in one
543
+ `wrangler secret bulk` stdin payload, and commits a receipt. A failed push
544
+ aborts the session and revokes partial issuance.
545
+
546
+ Plain reruns are idempotent at the tenant/configuration layer. Each deployment
547
+ receives new, independently revocable runtime credentials; it never rotates a
548
+ sibling developer or Worker. `--rotate-o11y-token` remains a destructive legacy
549
+ recovery operation for locally cached credentials, and `--rotate-keys` remains
550
+ the broader local rotation. Neither rotation flag may be combined with
551
+ `--push-secrets`. Production mutation or secret transfer requires
552
+ `--yes`. `--no-write-credentials` is valid with `--push-secrets` because the
553
+ deployment path is memory-only by design.
554
+
555
+ Registry staging and Wrangler installation use a short-lived receipt protocol.
556
+ The CLI preflights the exact target, issues additive credentials, sends one
557
+ bulk payload, and commits only after Wrangler succeeds. Failure revokes staged
558
+ credentials; retrying creates a new bounded session and never invalidates a
559
+ working sibling runtime.
560
+
561
+ Inventory and retire one successful deployment without disturbing any sibling:
562
+
563
+ ```sh
564
+ npx @odla-ai/cli credentials list --env dev
565
+ npx @odla-ai/cli credentials revoke <receipt-id>
566
+ ```
567
+
568
+ The inventory contains only receipt, target, state, and timestamp metadata.
569
+ Revocation uses the receipt's exact DB and o11y credential ids; no value is
570
+ retrieved or rotated.
553
571
 
554
572
  ### Application capability integrations
555
573
 
@@ -797,9 +815,10 @@ in an agent's transcript. It preflights `wrangler whoami` and the presence of a
797
815
  wrangler config file. The env `prod`/`production` targets the top-level
798
816
  wrangler environment (no `--env` flag is passed to Wrangler) and requires
799
817
  `--yes`; every other env maps to `wrangler --env <name>`. `--dry-run` prints a
800
- redacted plan without spawning anything. For the normal path, use
801
- `provision --write-dev-vars --push-secrets` so service enablement, credential
802
- issuance, local config, and deployed secret transfer stay one composition.
818
+ redacted plan without spawning anything. It is only for an already-saved local
819
+ credential. For a new Worker or a lost local plaintext, use
820
+ `provision --push-secrets`: the handshake authorizes new target-bound
821
+ credentials and no local production cache is required.
803
822
 
804
823
  `secrets set <name> --env <env>` stores one named secret in that env's tenant
805
824
  vault — the same write-only slot Studio's Secrets panel fills (for example
package/dist/bin.cjs CHANGED
@@ -4376,6 +4376,41 @@ async function wranglerLoggedIn(run, cwd) {
4376
4376
  return false;
4377
4377
  }
4378
4378
  }
4379
+ async function wranglerRuntimeTarget(run, opts) {
4380
+ const configPath = findWranglerConfig(opts.cwd);
4381
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
4382
+ const config = readWranglerConfig(configPath);
4383
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
4384
+ const baseName = typeof config.name === "string" ? config.name : "";
4385
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
4386
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
4387
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
4388
+ const serviceEnvironments = config.legacy_env === false;
4389
+ if (serviceEnvironments && explicitName) {
4390
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
4391
+ }
4392
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
4393
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
4394
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
4395
+ }
4396
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
4397
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
4398
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
4399
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
4400
+ }
4401
+ const discovered = [...new Set(`${whoami.stdout}
4402
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
4403
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
4404
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
4405
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
4406
+ }
4407
+ return {
4408
+ provider: "cloudflare",
4409
+ accountId,
4410
+ scriptName,
4411
+ ...opts.env ? { environment: opts.env } : {}
4412
+ };
4413
+ }
4379
4414
  function wranglerPutSecret(run, opts) {
4380
4415
  const args = [
4381
4416
  "wrangler",
@@ -4387,6 +4422,16 @@ function wranglerPutSecret(run, opts) {
4387
4422
  ];
4388
4423
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
4389
4424
  }
4425
+ function wranglerBulkSecrets(run, opts) {
4426
+ const args = [
4427
+ "wrangler",
4428
+ "secret",
4429
+ "bulk",
4430
+ ...opts.env ? ["--env", opts.env] : [],
4431
+ ...opts.configPath ? ["--config", opts.configPath] : []
4432
+ ];
4433
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4434
+ }
4390
4435
  var import_node_child_process2, import_node_fs12, import_node_path11, defaultRunner, WRANGLER_CONFIG_FILES;
4391
4436
  var init_wrangler = __esm({
4392
4437
  "src/wrangler.ts"() {
@@ -4947,9 +4992,6 @@ var init_init = __esm({
4947
4992
  async function secretsPush(options) {
4948
4993
  await secretsPushImpl(options, true);
4949
4994
  }
4950
- async function secretsPushAfterPreflight(options) {
4951
- await secretsPushImpl(options, false);
4952
- }
4953
4995
  async function secretsPushImpl(options, preflight) {
4954
4996
  const out = options.stdout ?? console;
4955
4997
  const cfg = await loadProjectConfig(options.configPath);
@@ -9501,6 +9543,65 @@ var init_context_command = __esm({
9501
9543
  }
9502
9544
  });
9503
9545
 
9546
+ // src/credential-command.ts
9547
+ async function responseError(response2) {
9548
+ return redactSecrets((await response2.text()).slice(0, 1e3));
9549
+ }
9550
+ async function credentialCommand(parsed, deps = {}) {
9551
+ const action2 = parsed.positionals[1] ?? "list";
9552
+ if (action2 !== "list" && action2 !== "revoke") {
9553
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
9554
+ }
9555
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
9556
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9557
+ const doFetch = deps.fetch ?? fetch;
9558
+ const out = deps.stdout ?? console;
9559
+ const token = await getDeveloperToken(cfg, {
9560
+ configPath: cfg.configPath,
9561
+ token: stringOpt(parsed.options.token),
9562
+ email: stringOpt(parsed.options.email),
9563
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9564
+ openApprovalUrl: deps.openUrl
9565
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
9566
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
9567
+ if (action2 === "revoke") {
9568
+ const id = parsed.positionals[2];
9569
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
9570
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
9571
+ method: "DELETE",
9572
+ headers: { authorization: `Bearer ${token}` }
9573
+ });
9574
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
9575
+ const body2 = await response3.json();
9576
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
9577
+ }
9578
+ const query = new URLSearchParams();
9579
+ const requestedEnv = stringOpt(parsed.options.env);
9580
+ if (requestedEnv) query.set("env", requestedEnv);
9581
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
9582
+ headers: { authorization: `Bearer ${token}` }
9583
+ });
9584
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
9585
+ const body = await response2.json();
9586
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
9587
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
9588
+ if (!credentials.length) return out.log("no active runtime credential receipts");
9589
+ out.log("RECEIPT ENV TARGET CREATED");
9590
+ for (const item of credentials) {
9591
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
9592
+ }
9593
+ }
9594
+ var init_credential_command = __esm({
9595
+ "src/credential-command.ts"() {
9596
+ "use strict";
9597
+ init_cjs_shims();
9598
+ init_argv();
9599
+ init_config();
9600
+ init_redact();
9601
+ init_token();
9602
+ }
9603
+ });
9604
+
9504
9605
  // src/help-usage.ts
9505
9606
  var USAGE_SECTION;
9506
9607
  var init_help_usage = __esm({
@@ -9624,6 +9725,8 @@ Usage:
9624
9725
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
9625
9726
  odla-ai security run [target] --self --ack-redacted-source
9626
9727
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
9728
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
9729
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
9627
9730
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
9628
9731
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
9629
9732
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -9748,9 +9851,13 @@ Safety:
9748
9851
  pinned versions of every external @odla-ai runtime module before importing
9749
9852
  the command graph. A stale workspace module blocks with its resolved path and
9750
9853
  tells the agent to update/rebase, npm ci, and rebuild.
9751
- Provision caches the approved developer token and service credentials under
9752
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
9753
- preflights Wrangler before any shown-once issuance or destructive rotation.
9854
+ Provision caches the approved developer token under .odla/ with mode 0600.
9855
+ Local-only development provisioning may also cache that developer's service
9856
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9857
+ independently revocable credential set through the approved handshake,
9858
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9859
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9860
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
9754
9861
  Projectless PM, Discussions, o11y, runbook, and identity commands use
9755
9862
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
9756
9863
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -11829,7 +11936,7 @@ async function issueO11yToken(opts) {
11829
11936
  );
11830
11937
  if (res.status === 409 && !opts.rotateO11y) {
11831
11938
  throw new Error(
11832
- `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
11939
+ `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
11833
11940
  );
11834
11941
  }
11835
11942
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -11855,6 +11962,97 @@ var init_provision_credentials = __esm({
11855
11962
  }
11856
11963
  });
11857
11964
 
11965
+ // src/runtime-credentials.ts
11966
+ function runtimeUrl(cfg, suffix = "") {
11967
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
11968
+ }
11969
+ async function safeError(response2) {
11970
+ const text2 = await response2.text();
11971
+ return redactSecrets(text2.slice(0, 1e3));
11972
+ }
11973
+ async function finish(doFetch, cfg, token, sessionId, method) {
11974
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
11975
+ method,
11976
+ headers: { authorization: `Bearer ${token}` }
11977
+ });
11978
+ }
11979
+ async function deliverRuntimeCredentials(cfg, options) {
11980
+ const doFetch = options.fetch ?? fetch;
11981
+ const run = options.runner ?? defaultRunner;
11982
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
11983
+ const target = await wranglerRuntimeTarget(run, {
11984
+ cwd: cfg.rootDir,
11985
+ env: wranglerEnv
11986
+ });
11987
+ const issue = await doFetch(runtimeUrl(cfg), {
11988
+ method: "POST",
11989
+ headers: {
11990
+ authorization: `Bearer ${options.developerToken}`,
11991
+ "content-type": "application/json"
11992
+ },
11993
+ body: JSON.stringify({
11994
+ env: options.env,
11995
+ idempotencyKey: `wrangler:${(0, import_node_crypto5.randomUUID)()}`,
11996
+ target
11997
+ })
11998
+ });
11999
+ const body = await issue.json().catch(() => null);
12000
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
12001
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
12002
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
12003
+ }
12004
+ const secrets = body.secrets;
12005
+ const values = {};
12006
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
12007
+ const value2 = secrets[name];
12008
+ if (typeof value2 === "string" && value2) values[name] = value2;
12009
+ }
12010
+ if (Object.keys(values).length === 0) {
12011
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
12012
+ throw new Error("runtime credential staging returned no configured service credentials");
12013
+ }
12014
+ const pushed = await wranglerBulkSecrets(run, {
12015
+ secrets: values,
12016
+ env: wranglerEnv,
12017
+ cwd: cfg.rootDir
12018
+ });
12019
+ if (pushed.code !== 0) {
12020
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
12021
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
12022
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
12023
+ }
12024
+ const committed = await finish(
12025
+ doFetch,
12026
+ cfg,
12027
+ options.developerToken,
12028
+ body.sessionId,
12029
+ "POST"
12030
+ );
12031
+ if (!committed.ok) {
12032
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
12033
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
12034
+ }
12035
+ options.stdout?.log(
12036
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
12037
+ );
12038
+ return {
12039
+ sessionId: body.sessionId,
12040
+ target,
12041
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
12042
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
12043
+ };
12044
+ }
12045
+ var import_node_crypto5;
12046
+ var init_runtime_credentials = __esm({
12047
+ "src/runtime-credentials.ts"() {
12048
+ "use strict";
12049
+ init_cjs_shims();
12050
+ import_node_crypto5 = require("crypto");
12051
+ init_redact();
12052
+ init_wrangler();
12053
+ }
12054
+ });
12055
+
11858
12056
  // src/provision-live.ts
11859
12057
  function liveProvisionConfig(cfg) {
11860
12058
  if (!cfg.envs.includes("dev")) {
@@ -11894,8 +12092,10 @@ async function provision(options) {
11894
12092
  if (options.rotateO11yToken && !hasO11y) {
11895
12093
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
11896
12094
  }
11897
- if (options.pushSecrets && options.writeCredentials === false) {
11898
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
12095
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
12096
+ throw new Error(
12097
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
12098
+ );
11899
12099
  }
11900
12100
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
11901
12101
  out.log(` platform: ${plan.platformUrl}`);
@@ -11932,7 +12132,7 @@ async function provision(options) {
11932
12132
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
11933
12133
  return;
11934
12134
  }
11935
- let credentials = readCredentials(cfg.local.credentialsFile);
12135
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
11936
12136
  if (credentials && credentials.appId !== cfg.app.id) {
11937
12137
  throw new Error(
11938
12138
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -11941,7 +12141,7 @@ async function provision(options) {
11941
12141
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
11942
12142
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
11943
12143
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
11944
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
12144
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
11945
12145
  if (options.writeCredentials === false && losesShownOnceCredential) {
11946
12146
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
11947
12147
  }
@@ -12015,38 +12215,42 @@ async function provision(options) {
12015
12215
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
12016
12216
  }
12017
12217
  }
12218
+ let devVarsCredentials = credentials;
12018
12219
  for (const env of cfg.envs) {
12019
12220
  const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
12020
- credentials = await provisionEnvCredentials({
12021
- cfg,
12022
- env,
12023
- developerToken: token,
12024
- credentials,
12025
- rotateDb: !!options.rotateKeys,
12026
- rotateO11y: rotatesO11y,
12027
- write: options.writeCredentials !== false,
12028
- fetch: doFetch,
12029
- stdout: out
12030
- });
12031
- const dbKey = credentials.envs[env]?.dbKey;
12221
+ let dbKey;
12032
12222
  if (options.pushSecrets) {
12033
- try {
12034
- await secretsPushAfterPreflight({
12035
- configPath: cfg.configPath,
12036
- env,
12037
- yes: options.yes,
12038
- runner: options.secretRunner,
12039
- stdout: out
12040
- });
12041
- } catch (error) {
12042
- const message2 = error instanceof Error ? error.message : String(error);
12043
- const consent = env === "prod" || env === "production" ? " --yes" : "";
12044
- throw new Error(
12045
- `${message2}
12046
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
12047
- { cause: error }
12048
- );
12049
- }
12223
+ const delivered = await deliverRuntimeCredentials(cfg, {
12224
+ env,
12225
+ developerToken: token,
12226
+ fetch: doFetch,
12227
+ runner: options.secretRunner,
12228
+ stdout: out
12229
+ });
12230
+ dbKey = delivered.dbKey;
12231
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
12232
+ appId: cfg.app.id,
12233
+ platformUrl: cfg.platformUrl,
12234
+ dbEndpoint: cfg.dbEndpoint,
12235
+ env,
12236
+ tenantId,
12237
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
12238
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
12239
+ });
12240
+ } else {
12241
+ credentials = await provisionEnvCredentials({
12242
+ cfg,
12243
+ env,
12244
+ developerToken: token,
12245
+ credentials,
12246
+ rotateDb: !!options.rotateKeys,
12247
+ rotateO11y: rotatesO11y,
12248
+ write: options.writeCredentials !== false,
12249
+ fetch: doFetch,
12250
+ stdout: out
12251
+ });
12252
+ devVarsCredentials = credentials;
12253
+ dbKey = credentials.envs[env]?.dbKey;
12050
12254
  }
12051
12255
  if (schema && dbKey) {
12052
12256
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -12070,13 +12274,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
12070
12274
  }
12071
12275
  }
12072
12276
  }
12073
- if (options.writeCredentials !== false && credentials) {
12277
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
12074
12278
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
12075
12279
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
12076
12280
  }
12077
- if (devVarsTarget && credentials) {
12281
+ if (devVarsTarget && devVarsCredentials) {
12078
12282
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
12079
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
12283
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
12080
12284
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
12081
12285
  }
12082
12286
  if (cfg.services.includes("calendar")) {
@@ -12116,6 +12320,7 @@ var init_provision = __esm({
12116
12320
  init_token();
12117
12321
  init_local();
12118
12322
  init_provision_credentials();
12323
+ init_runtime_credentials();
12119
12324
  init_provision_live();
12120
12325
  init_provision_helpers();
12121
12326
  init_secrets();
@@ -12226,6 +12431,7 @@ var init_surface = __esm({
12226
12431
  code: { connect: {} },
12227
12432
  config: { diff: {}, plan: {}, apply: {} },
12228
12433
  context: { show: {}, list: {}, save: {}, remove: {} },
12434
+ credentials: { list: {}, revoke: {} },
12229
12435
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
12230
12436
  discuss: {
12231
12437
  groups: {},
@@ -14022,6 +14228,10 @@ async function runCli(argv2 = process.argv.slice(2), dependencies = {}) {
14022
14228
  await contextCommand(parsed, runtime);
14023
14229
  return;
14024
14230
  }
14231
+ if (command === "credentials") {
14232
+ await credentialCommand(parsed, runtime);
14233
+ return;
14234
+ }
14025
14235
  if (command === "runbook") {
14026
14236
  await runbookCommand(parsed, runtime);
14027
14237
  return;
@@ -14164,6 +14374,7 @@ var init_cli = __esm({
14164
14374
  init_cli_project();
14165
14375
  init_code_command();
14166
14376
  init_context_command();
14377
+ init_credential_command();
14167
14378
  init_help();
14168
14379
  init_version();
14169
14380
  init_discuss_command();