@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/dist/bin.js CHANGED
@@ -174,7 +174,7 @@ function absoluteEntryPath(entryPath) {
174
174
 
175
175
  // src/bin.ts
176
176
  var argv = process.argv.slice(2);
177
- requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-DNOAYDBR.js")).runCli()).catch((err) => {
177
+ requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-CKBUVTJM.js")).runCli()).catch((err) => {
178
178
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
179
179
  process.exitCode = exitCodeFor(err);
180
180
  });
@@ -3831,6 +3831,41 @@ async function wranglerLoggedIn(run, cwd) {
3831
3831
  return false;
3832
3832
  }
3833
3833
  }
3834
+ async function wranglerRuntimeTarget(run, opts) {
3835
+ const configPath = findWranglerConfig(opts.cwd);
3836
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
3837
+ const config = readWranglerConfig(configPath);
3838
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
3839
+ const baseName = typeof config.name === "string" ? config.name : "";
3840
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
3841
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
3842
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
3843
+ const serviceEnvironments = config.legacy_env === false;
3844
+ if (serviceEnvironments && explicitName) {
3845
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
3846
+ }
3847
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
3848
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
3849
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
3850
+ }
3851
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
3852
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
3853
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
3854
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
3855
+ }
3856
+ const discovered = [...new Set(`${whoami.stdout}
3857
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
3858
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
3859
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
3860
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
3861
+ }
3862
+ return {
3863
+ provider: "cloudflare",
3864
+ accountId,
3865
+ scriptName,
3866
+ ...opts.env ? { environment: opts.env } : {}
3867
+ };
3868
+ }
3834
3869
  function wranglerPutSecret(run, opts) {
3835
3870
  const args = [
3836
3871
  "wrangler",
@@ -3842,6 +3877,16 @@ function wranglerPutSecret(run, opts) {
3842
3877
  ];
3843
3878
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3844
3879
  }
3880
+ function wranglerBulkSecrets(run, opts) {
3881
+ const args = [
3882
+ "wrangler",
3883
+ "secret",
3884
+ "bulk",
3885
+ ...opts.env ? ["--env", opts.env] : [],
3886
+ ...opts.configPath ? ["--config", opts.configPath] : []
3887
+ ];
3888
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
3889
+ }
3845
3890
 
3846
3891
  // src/doctor-checks.ts
3847
3892
  function lintRules(rules, entities, publicRead) {
@@ -4330,9 +4375,6 @@ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
4330
4375
  async function secretsPush(options) {
4331
4376
  await secretsPushImpl(options, true);
4332
4377
  }
4333
- async function secretsPushAfterPreflight(options) {
4334
- await secretsPushImpl(options, false);
4335
- }
4336
4378
  async function secretsPushImpl(options, preflight) {
4337
4379
  const out = options.stdout ?? console;
4338
4380
  const cfg = await loadProjectConfig(options.configPath);
@@ -8697,6 +8739,55 @@ function requireName(parsed) {
8697
8739
  return name;
8698
8740
  }
8699
8741
 
8742
+ // src/credential-command.ts
8743
+ async function responseError(response2) {
8744
+ return redactSecrets((await response2.text()).slice(0, 1e3));
8745
+ }
8746
+ async function credentialCommand(parsed, deps = {}) {
8747
+ const action2 = parsed.positionals[1] ?? "list";
8748
+ if (action2 !== "list" && action2 !== "revoke") {
8749
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
8750
+ }
8751
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
8752
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8753
+ const doFetch = deps.fetch ?? fetch;
8754
+ const out = deps.stdout ?? console;
8755
+ const token = await getDeveloperToken(cfg, {
8756
+ configPath: cfg.configPath,
8757
+ token: stringOpt(parsed.options.token),
8758
+ email: stringOpt(parsed.options.email),
8759
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
8760
+ openApprovalUrl: deps.openUrl
8761
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
8762
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
8763
+ if (action2 === "revoke") {
8764
+ const id = parsed.positionals[2];
8765
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
8766
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
8767
+ method: "DELETE",
8768
+ headers: { authorization: `Bearer ${token}` }
8769
+ });
8770
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
8771
+ const body2 = await response3.json();
8772
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
8773
+ }
8774
+ const query = new URLSearchParams();
8775
+ const requestedEnv = stringOpt(parsed.options.env);
8776
+ if (requestedEnv) query.set("env", requestedEnv);
8777
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
8778
+ headers: { authorization: `Bearer ${token}` }
8779
+ });
8780
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
8781
+ const body = await response2.json();
8782
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
8783
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
8784
+ if (!credentials.length) return out.log("no active runtime credential receipts");
8785
+ out.log("RECEIPT ENV TARGET CREATED");
8786
+ for (const item of credentials) {
8787
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
8788
+ }
8789
+ }
8790
+
8700
8791
  // src/help-usage.ts
8701
8792
  var USAGE_SECTION = `
8702
8793
  Start here:
@@ -8815,6 +8906,8 @@ Usage:
8815
8906
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8816
8907
  odla-ai security run [target] --self --ack-redacted-source
8817
8908
  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]
8909
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
8910
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
8818
8911
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8819
8912
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8820
8913
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -8937,9 +9030,13 @@ Safety:
8937
9030
  pinned versions of every external @odla-ai runtime module before importing
8938
9031
  the command graph. A stale workspace module blocks with its resolved path and
8939
9032
  tells the agent to update/rebase, npm ci, and rebuild.
8940
- Provision caches the approved developer token and service credentials under
8941
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
8942
- preflights Wrangler before any shown-once issuance or destructive rotation.
9033
+ Provision caches the approved developer token under .odla/ with mode 0600.
9034
+ Local-only development provisioning may also cache that developer's service
9035
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9036
+ independently revocable credential set through the approved handshake,
9037
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9038
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9039
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
8943
9040
  Projectless PM, Discussions, o11y, runbook, and identity commands use
8944
9041
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
8945
9042
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -10863,7 +10960,7 @@ async function issueO11yToken(opts) {
10863
10960
  );
10864
10961
  if (res.status === 409 && !opts.rotateO11y) {
10865
10962
  throw new Error(
10866
- `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`
10963
+ `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`
10867
10964
  );
10868
10965
  }
10869
10966
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -10879,6 +10976,88 @@ async function safeText7(res) {
10879
10976
  }
10880
10977
  }
10881
10978
 
10979
+ // src/runtime-credentials.ts
10980
+ import { randomUUID as randomUUID3 } from "crypto";
10981
+ function runtimeUrl(cfg, suffix = "") {
10982
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
10983
+ }
10984
+ async function safeError(response2) {
10985
+ const text2 = await response2.text();
10986
+ return redactSecrets(text2.slice(0, 1e3));
10987
+ }
10988
+ async function finish(doFetch, cfg, token, sessionId, method) {
10989
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
10990
+ method,
10991
+ headers: { authorization: `Bearer ${token}` }
10992
+ });
10993
+ }
10994
+ async function deliverRuntimeCredentials(cfg, options) {
10995
+ const doFetch = options.fetch ?? fetch;
10996
+ const run = options.runner ?? defaultRunner;
10997
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
10998
+ const target = await wranglerRuntimeTarget(run, {
10999
+ cwd: cfg.rootDir,
11000
+ env: wranglerEnv
11001
+ });
11002
+ const issue = await doFetch(runtimeUrl(cfg), {
11003
+ method: "POST",
11004
+ headers: {
11005
+ authorization: `Bearer ${options.developerToken}`,
11006
+ "content-type": "application/json"
11007
+ },
11008
+ body: JSON.stringify({
11009
+ env: options.env,
11010
+ idempotencyKey: `wrangler:${randomUUID3()}`,
11011
+ target
11012
+ })
11013
+ });
11014
+ const body = await issue.json().catch(() => null);
11015
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
11016
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
11017
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
11018
+ }
11019
+ const secrets = body.secrets;
11020
+ const values = {};
11021
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
11022
+ const value2 = secrets[name];
11023
+ if (typeof value2 === "string" && value2) values[name] = value2;
11024
+ }
11025
+ if (Object.keys(values).length === 0) {
11026
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11027
+ throw new Error("runtime credential staging returned no configured service credentials");
11028
+ }
11029
+ const pushed = await wranglerBulkSecrets(run, {
11030
+ secrets: values,
11031
+ env: wranglerEnv,
11032
+ cwd: cfg.rootDir
11033
+ });
11034
+ if (pushed.code !== 0) {
11035
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11036
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
11037
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
11038
+ }
11039
+ const committed = await finish(
11040
+ doFetch,
11041
+ cfg,
11042
+ options.developerToken,
11043
+ body.sessionId,
11044
+ "POST"
11045
+ );
11046
+ if (!committed.ok) {
11047
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11048
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
11049
+ }
11050
+ options.stdout?.log(
11051
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
11052
+ );
11053
+ return {
11054
+ sessionId: body.sessionId,
11055
+ target,
11056
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
11057
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
11058
+ };
11059
+ }
11060
+
10882
11061
  // src/provision-live.ts
10883
11062
  function liveProvisionConfig(cfg) {
10884
11063
  if (!cfg.envs.includes("dev")) {
@@ -10912,8 +11091,10 @@ async function provision(options) {
10912
11091
  if (options.rotateO11yToken && !hasO11y) {
10913
11092
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
10914
11093
  }
10915
- if (options.pushSecrets && options.writeCredentials === false) {
10916
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
11094
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
11095
+ throw new Error(
11096
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
11097
+ );
10917
11098
  }
10918
11099
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
10919
11100
  out.log(` platform: ${plan.platformUrl}`);
@@ -10950,7 +11131,7 @@ async function provision(options) {
10950
11131
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
10951
11132
  return;
10952
11133
  }
10953
- let credentials = readCredentials(cfg.local.credentialsFile);
11134
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
10954
11135
  if (credentials && credentials.appId !== cfg.app.id) {
10955
11136
  throw new Error(
10956
11137
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -10959,7 +11140,7 @@ async function provision(options) {
10959
11140
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
10960
11141
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
10961
11142
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
10962
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
11143
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
10963
11144
  if (options.writeCredentials === false && losesShownOnceCredential) {
10964
11145
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
10965
11146
  }
@@ -11033,38 +11214,42 @@ async function provision(options) {
11033
11214
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
11034
11215
  }
11035
11216
  }
11217
+ let devVarsCredentials = credentials;
11036
11218
  for (const env of cfg.envs) {
11037
11219
  const tenantId = tenantIdFor5(cfg.app.id, env);
11038
- credentials = await provisionEnvCredentials({
11039
- cfg,
11040
- env,
11041
- developerToken: token,
11042
- credentials,
11043
- rotateDb: !!options.rotateKeys,
11044
- rotateO11y: rotatesO11y,
11045
- write: options.writeCredentials !== false,
11046
- fetch: doFetch,
11047
- stdout: out
11048
- });
11049
- const dbKey = credentials.envs[env]?.dbKey;
11220
+ let dbKey;
11050
11221
  if (options.pushSecrets) {
11051
- try {
11052
- await secretsPushAfterPreflight({
11053
- configPath: cfg.configPath,
11054
- env,
11055
- yes: options.yes,
11056
- runner: options.secretRunner,
11057
- stdout: out
11058
- });
11059
- } catch (error) {
11060
- const message2 = error instanceof Error ? error.message : String(error);
11061
- const consent = env === "prod" || env === "production" ? " --yes" : "";
11062
- throw new Error(
11063
- `${message2}
11064
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
11065
- { cause: error }
11066
- );
11067
- }
11222
+ const delivered = await deliverRuntimeCredentials(cfg, {
11223
+ env,
11224
+ developerToken: token,
11225
+ fetch: doFetch,
11226
+ runner: options.secretRunner,
11227
+ stdout: out
11228
+ });
11229
+ dbKey = delivered.dbKey;
11230
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
11231
+ appId: cfg.app.id,
11232
+ platformUrl: cfg.platformUrl,
11233
+ dbEndpoint: cfg.dbEndpoint,
11234
+ env,
11235
+ tenantId,
11236
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
11237
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
11238
+ });
11239
+ } else {
11240
+ credentials = await provisionEnvCredentials({
11241
+ cfg,
11242
+ env,
11243
+ developerToken: token,
11244
+ credentials,
11245
+ rotateDb: !!options.rotateKeys,
11246
+ rotateO11y: rotatesO11y,
11247
+ write: options.writeCredentials !== false,
11248
+ fetch: doFetch,
11249
+ stdout: out
11250
+ });
11251
+ devVarsCredentials = credentials;
11252
+ dbKey = credentials.envs[env]?.dbKey;
11068
11253
  }
11069
11254
  if (schema && dbKey) {
11070
11255
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -11088,13 +11273,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11088
11273
  }
11089
11274
  }
11090
11275
  }
11091
- if (options.writeCredentials !== false && credentials) {
11276
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
11092
11277
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
11093
11278
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
11094
11279
  }
11095
- if (devVarsTarget && credentials) {
11280
+ if (devVarsTarget && devVarsCredentials) {
11096
11281
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
11097
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
11282
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
11098
11283
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
11099
11284
  }
11100
11285
  if (cfg.services.includes("calendar")) {
@@ -11185,6 +11370,7 @@ var COMMAND_SURFACE = {
11185
11370
  code: { connect: {} },
11186
11371
  config: { diff: {}, plan: {}, apply: {} },
11187
11372
  context: { show: {}, list: {}, save: {}, remove: {} },
11373
+ credentials: { list: {}, revoke: {} },
11188
11374
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
11189
11375
  discuss: {
11190
11376
  groups: {},
@@ -12869,6 +13055,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12869
13055
  await contextCommand(parsed, runtime);
12870
13056
  return;
12871
13057
  }
13058
+ if (command === "credentials") {
13059
+ await credentialCommand(parsed, runtime);
13060
+ return;
13061
+ }
12872
13062
  if (command === "runbook") {
12873
13063
  await runbookCommand(parsed, runtime);
12874
13064
  return;
@@ -13056,4 +13246,4 @@ export {
13056
13246
  isTerminalHostedSecurityStatus,
13057
13247
  runCli
13058
13248
  };
13059
- //# sourceMappingURL=chunk-YTVLTADT.js.map
13249
+ //# sourceMappingURL=chunk-PPSXHZJI.js.map