@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-YTVLTADT.js";
4
+ } from "./chunk-PPSXHZJI.js";
5
5
  import {
6
6
  exitCodeFor
7
7
  } from "./chunk-UKLSRQ5J.js";
@@ -9,4 +9,4 @@ export {
9
9
  exitCodeFor,
10
10
  runCli
11
11
  };
12
- //# sourceMappingURL=cli-DNOAYDBR.js.map
12
+ //# sourceMappingURL=cli-CKBUVTJM.js.map
package/dist/index.cjs CHANGED
@@ -3961,6 +3961,41 @@ async function wranglerLoggedIn(run, cwd) {
3961
3961
  return false;
3962
3962
  }
3963
3963
  }
3964
+ async function wranglerRuntimeTarget(run, opts) {
3965
+ const configPath = findWranglerConfig(opts.cwd);
3966
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
3967
+ const config = readWranglerConfig(configPath);
3968
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
3969
+ const baseName = typeof config.name === "string" ? config.name : "";
3970
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
3971
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
3972
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
3973
+ const serviceEnvironments = config.legacy_env === false;
3974
+ if (serviceEnvironments && explicitName) {
3975
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
3976
+ }
3977
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
3978
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
3979
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
3980
+ }
3981
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
3982
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
3983
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
3984
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
3985
+ }
3986
+ const discovered = [...new Set(`${whoami.stdout}
3987
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
3988
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
3989
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
3990
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
3991
+ }
3992
+ return {
3993
+ provider: "cloudflare",
3994
+ accountId,
3995
+ scriptName,
3996
+ ...opts.env ? { environment: opts.env } : {}
3997
+ };
3998
+ }
3964
3999
  function wranglerPutSecret(run, opts) {
3965
4000
  const args = [
3966
4001
  "wrangler",
@@ -3972,6 +4007,16 @@ function wranglerPutSecret(run, opts) {
3972
4007
  ];
3973
4008
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3974
4009
  }
4010
+ function wranglerBulkSecrets(run, opts) {
4011
+ const args = [
4012
+ "wrangler",
4013
+ "secret",
4014
+ "bulk",
4015
+ ...opts.env ? ["--env", opts.env] : [],
4016
+ ...opts.configPath ? ["--config", opts.configPath] : []
4017
+ ];
4018
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4019
+ }
3975
4020
 
3976
4021
  // src/doctor-checks.ts
3977
4022
  function lintRules(rules, entities, publicRead) {
@@ -4460,9 +4505,6 @@ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
4460
4505
  async function secretsPush(options) {
4461
4506
  await secretsPushImpl(options, true);
4462
4507
  }
4463
- async function secretsPushAfterPreflight(options) {
4464
- await secretsPushImpl(options, false);
4465
- }
4466
4508
  async function secretsPushImpl(options, preflight) {
4467
4509
  const out = options.stdout ?? console;
4468
4510
  const cfg = await loadProjectConfig(options.configPath);
@@ -8827,6 +8869,55 @@ function requireName(parsed) {
8827
8869
  return name;
8828
8870
  }
8829
8871
 
8872
+ // src/credential-command.ts
8873
+ async function responseError(response2) {
8874
+ return redactSecrets((await response2.text()).slice(0, 1e3));
8875
+ }
8876
+ async function credentialCommand(parsed, deps = {}) {
8877
+ const action2 = parsed.positionals[1] ?? "list";
8878
+ if (action2 !== "list" && action2 !== "revoke") {
8879
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
8880
+ }
8881
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
8882
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8883
+ const doFetch = deps.fetch ?? fetch;
8884
+ const out = deps.stdout ?? console;
8885
+ const token = await getDeveloperToken(cfg, {
8886
+ configPath: cfg.configPath,
8887
+ token: stringOpt(parsed.options.token),
8888
+ email: stringOpt(parsed.options.email),
8889
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
8890
+ openApprovalUrl: deps.openUrl
8891
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
8892
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
8893
+ if (action2 === "revoke") {
8894
+ const id = parsed.positionals[2];
8895
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
8896
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
8897
+ method: "DELETE",
8898
+ headers: { authorization: `Bearer ${token}` }
8899
+ });
8900
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
8901
+ const body2 = await response3.json();
8902
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
8903
+ }
8904
+ const query = new URLSearchParams();
8905
+ const requestedEnv = stringOpt(parsed.options.env);
8906
+ if (requestedEnv) query.set("env", requestedEnv);
8907
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
8908
+ headers: { authorization: `Bearer ${token}` }
8909
+ });
8910
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
8911
+ const body = await response2.json();
8912
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
8913
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
8914
+ if (!credentials.length) return out.log("no active runtime credential receipts");
8915
+ out.log("RECEIPT ENV TARGET CREATED");
8916
+ for (const item of credentials) {
8917
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
8918
+ }
8919
+ }
8920
+
8830
8921
  // src/help-usage.ts
8831
8922
  var USAGE_SECTION = `
8832
8923
  Start here:
@@ -8945,6 +9036,8 @@ Usage:
8945
9036
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8946
9037
  odla-ai security run [target] --self --ack-redacted-source
8947
9038
  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]
9039
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
9040
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
8948
9041
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8949
9042
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8950
9043
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -9067,9 +9160,13 @@ Safety:
9067
9160
  pinned versions of every external @odla-ai runtime module before importing
9068
9161
  the command graph. A stale workspace module blocks with its resolved path and
9069
9162
  tells the agent to update/rebase, npm ci, and rebuild.
9070
- Provision caches the approved developer token and service credentials under
9071
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
9072
- preflights Wrangler before any shown-once issuance or destructive rotation.
9163
+ Provision caches the approved developer token under .odla/ with mode 0600.
9164
+ Local-only development provisioning may also cache that developer's service
9165
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9166
+ independently revocable credential set through the approved handshake,
9167
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9168
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9169
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
9073
9170
  Projectless PM, Discussions, o11y, runbook, and identity commands use
9074
9171
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
9075
9172
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -10993,7 +11090,7 @@ async function issueO11yToken(opts) {
10993
11090
  );
10994
11091
  if (res.status === 409 && !opts.rotateO11y) {
10995
11092
  throw new Error(
10996
- `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`
11093
+ `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`
10997
11094
  );
10998
11095
  }
10999
11096
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -11009,6 +11106,88 @@ async function safeText7(res) {
11009
11106
  }
11010
11107
  }
11011
11108
 
11109
+ // src/runtime-credentials.ts
11110
+ var import_node_crypto5 = require("crypto");
11111
+ function runtimeUrl(cfg, suffix = "") {
11112
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
11113
+ }
11114
+ async function safeError(response2) {
11115
+ const text2 = await response2.text();
11116
+ return redactSecrets(text2.slice(0, 1e3));
11117
+ }
11118
+ async function finish(doFetch, cfg, token, sessionId, method) {
11119
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
11120
+ method,
11121
+ headers: { authorization: `Bearer ${token}` }
11122
+ });
11123
+ }
11124
+ async function deliverRuntimeCredentials(cfg, options) {
11125
+ const doFetch = options.fetch ?? fetch;
11126
+ const run = options.runner ?? defaultRunner;
11127
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
11128
+ const target = await wranglerRuntimeTarget(run, {
11129
+ cwd: cfg.rootDir,
11130
+ env: wranglerEnv
11131
+ });
11132
+ const issue = await doFetch(runtimeUrl(cfg), {
11133
+ method: "POST",
11134
+ headers: {
11135
+ authorization: `Bearer ${options.developerToken}`,
11136
+ "content-type": "application/json"
11137
+ },
11138
+ body: JSON.stringify({
11139
+ env: options.env,
11140
+ idempotencyKey: `wrangler:${(0, import_node_crypto5.randomUUID)()}`,
11141
+ target
11142
+ })
11143
+ });
11144
+ const body = await issue.json().catch(() => null);
11145
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
11146
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
11147
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
11148
+ }
11149
+ const secrets = body.secrets;
11150
+ const values = {};
11151
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
11152
+ const value2 = secrets[name];
11153
+ if (typeof value2 === "string" && value2) values[name] = value2;
11154
+ }
11155
+ if (Object.keys(values).length === 0) {
11156
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11157
+ throw new Error("runtime credential staging returned no configured service credentials");
11158
+ }
11159
+ const pushed = await wranglerBulkSecrets(run, {
11160
+ secrets: values,
11161
+ env: wranglerEnv,
11162
+ cwd: cfg.rootDir
11163
+ });
11164
+ if (pushed.code !== 0) {
11165
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11166
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
11167
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
11168
+ }
11169
+ const committed = await finish(
11170
+ doFetch,
11171
+ cfg,
11172
+ options.developerToken,
11173
+ body.sessionId,
11174
+ "POST"
11175
+ );
11176
+ if (!committed.ok) {
11177
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11178
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
11179
+ }
11180
+ options.stdout?.log(
11181
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
11182
+ );
11183
+ return {
11184
+ sessionId: body.sessionId,
11185
+ target,
11186
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
11187
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
11188
+ };
11189
+ }
11190
+
11012
11191
  // src/provision-live.ts
11013
11192
  function liveProvisionConfig(cfg) {
11014
11193
  if (!cfg.envs.includes("dev")) {
@@ -11042,8 +11221,10 @@ async function provision(options) {
11042
11221
  if (options.rotateO11yToken && !hasO11y) {
11043
11222
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
11044
11223
  }
11045
- if (options.pushSecrets && options.writeCredentials === false) {
11046
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
11224
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
11225
+ throw new Error(
11226
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
11227
+ );
11047
11228
  }
11048
11229
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
11049
11230
  out.log(` platform: ${plan.platformUrl}`);
@@ -11080,7 +11261,7 @@ async function provision(options) {
11080
11261
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
11081
11262
  return;
11082
11263
  }
11083
- let credentials = readCredentials(cfg.local.credentialsFile);
11264
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
11084
11265
  if (credentials && credentials.appId !== cfg.app.id) {
11085
11266
  throw new Error(
11086
11267
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -11089,7 +11270,7 @@ async function provision(options) {
11089
11270
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
11090
11271
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
11091
11272
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
11092
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
11273
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
11093
11274
  if (options.writeCredentials === false && losesShownOnceCredential) {
11094
11275
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
11095
11276
  }
@@ -11163,38 +11344,42 @@ async function provision(options) {
11163
11344
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
11164
11345
  }
11165
11346
  }
11347
+ let devVarsCredentials = credentials;
11166
11348
  for (const env of cfg.envs) {
11167
11349
  const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
11168
- credentials = await provisionEnvCredentials({
11169
- cfg,
11170
- env,
11171
- developerToken: token,
11172
- credentials,
11173
- rotateDb: !!options.rotateKeys,
11174
- rotateO11y: rotatesO11y,
11175
- write: options.writeCredentials !== false,
11176
- fetch: doFetch,
11177
- stdout: out
11178
- });
11179
- const dbKey = credentials.envs[env]?.dbKey;
11350
+ let dbKey;
11180
11351
  if (options.pushSecrets) {
11181
- try {
11182
- await secretsPushAfterPreflight({
11183
- configPath: cfg.configPath,
11184
- env,
11185
- yes: options.yes,
11186
- runner: options.secretRunner,
11187
- stdout: out
11188
- });
11189
- } catch (error) {
11190
- const message2 = error instanceof Error ? error.message : String(error);
11191
- const consent = env === "prod" || env === "production" ? " --yes" : "";
11192
- throw new Error(
11193
- `${message2}
11194
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
11195
- { cause: error }
11196
- );
11197
- }
11352
+ const delivered = await deliverRuntimeCredentials(cfg, {
11353
+ env,
11354
+ developerToken: token,
11355
+ fetch: doFetch,
11356
+ runner: options.secretRunner,
11357
+ stdout: out
11358
+ });
11359
+ dbKey = delivered.dbKey;
11360
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
11361
+ appId: cfg.app.id,
11362
+ platformUrl: cfg.platformUrl,
11363
+ dbEndpoint: cfg.dbEndpoint,
11364
+ env,
11365
+ tenantId,
11366
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
11367
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
11368
+ });
11369
+ } else {
11370
+ credentials = await provisionEnvCredentials({
11371
+ cfg,
11372
+ env,
11373
+ developerToken: token,
11374
+ credentials,
11375
+ rotateDb: !!options.rotateKeys,
11376
+ rotateO11y: rotatesO11y,
11377
+ write: options.writeCredentials !== false,
11378
+ fetch: doFetch,
11379
+ stdout: out
11380
+ });
11381
+ devVarsCredentials = credentials;
11382
+ dbKey = credentials.envs[env]?.dbKey;
11198
11383
  }
11199
11384
  if (schema && dbKey) {
11200
11385
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -11218,13 +11403,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11218
11403
  }
11219
11404
  }
11220
11405
  }
11221
- if (options.writeCredentials !== false && credentials) {
11406
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
11222
11407
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
11223
11408
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
11224
11409
  }
11225
- if (devVarsTarget && credentials) {
11410
+ if (devVarsTarget && devVarsCredentials) {
11226
11411
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
11227
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
11412
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
11228
11413
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
11229
11414
  }
11230
11415
  if (cfg.services.includes("calendar")) {
@@ -11315,6 +11500,7 @@ var COMMAND_SURFACE = {
11315
11500
  code: { connect: {} },
11316
11501
  config: { diff: {}, plan: {}, apply: {} },
11317
11502
  context: { show: {}, list: {}, save: {}, remove: {} },
11503
+ credentials: { list: {}, revoke: {} },
11318
11504
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
11319
11505
  discuss: {
11320
11506
  groups: {},
@@ -13052,6 +13238,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
13052
13238
  await contextCommand(parsed, runtime);
13053
13239
  return;
13054
13240
  }
13241
+ if (command === "credentials") {
13242
+ await credentialCommand(parsed, runtime);
13243
+ return;
13244
+ }
13055
13245
  if (command === "runbook") {
13056
13246
  await runbookCommand(parsed, runtime);
13057
13247
  return;