@odla-ai/cli 0.36.0 → 0.37.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.cjs CHANGED
@@ -401,9 +401,10 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
401
401
  });
402
402
  const body = await response2.json().catch(() => ({}));
403
403
  if (!response2.ok || typeof body.token !== "string") {
404
- const detail = body.error?.message ?? `registry returned ${response2.status}`;
404
+ const revocable = response2.status === 401 || response2.status === 403 || response2.status === 404;
405
+ const detail = body.error?.message ?? (response2.ok ? `registry returned ${response2.status} with no session token` : `registry returned ${response2.status}`);
405
406
  throw new Error(
406
- `device session failed: ${detail} (${response2.status}) \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio`
407
+ `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
407
408
  );
408
409
  }
409
410
  return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
@@ -837,6 +838,7 @@ var init_admin_ai_auth = __esm({
837
838
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
838
839
  "platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
839
840
  "app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
841
+ "platform:device:enroll:extended": "enrol this machine with a credential that outlives the seven-day owner ceiling \u2014 a durable secret on a machine you are vouching for",
840
842
  "platform:ai:policy:write": "change System AI model routing",
841
843
  "platform:ai:policy:read": "read System AI model routing",
842
844
  "platform:ai:credential:write": "replace a stored AI provider key",
@@ -11183,7 +11185,7 @@ Usage:
11183
11185
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
11184
11186
  odla-ai security run [target] --self --ack-redacted-source
11185
11187
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
11186
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
11188
+ odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
11187
11189
  odla-ai device list [--email <odla-account>] [--json]
11188
11190
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
11189
11191
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -14545,6 +14547,31 @@ var init_advisory_output = __esm({
14545
14547
  }
14546
14548
  });
14547
14549
 
14550
+ // src/device-ttl.ts
14551
+ function parseDeviceTtl(raw) {
14552
+ if (raw === void 0 || raw === true) return void 0;
14553
+ const text3 = String(raw).trim().toLowerCase();
14554
+ if (!text3) return void 0;
14555
+ if (text3 === "forever" || text3 === "never") return 100 * 365 * DAY_MS;
14556
+ const match = /^(\d+)\s*([dwy])$/.exec(text3);
14557
+ if (!match) {
14558
+ throw new Error(
14559
+ `--device-ttl expects a duration like 30d, 6w, 2y, or "forever" (got "${raw}")`
14560
+ );
14561
+ }
14562
+ return Number(match[1]) * UNITS[match[2]];
14563
+ }
14564
+ var OWNER_DEVICE_TTL_MS, DAY_MS, UNITS;
14565
+ var init_device_ttl = __esm({
14566
+ "src/device-ttl.ts"() {
14567
+ "use strict";
14568
+ init_cjs_shims();
14569
+ OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14570
+ DAY_MS = 24 * 60 * 60 * 1e3;
14571
+ UNITS = { d: DAY_MS, w: 7 * DAY_MS, y: 365 * DAY_MS };
14572
+ }
14573
+ });
14574
+
14548
14575
  // src/device-command.ts
14549
14576
  async function deviceCommand(parsed, deps) {
14550
14577
  const action2 = parsed.positionals[1] ?? "";
@@ -14561,7 +14588,17 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14561
14588
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
14562
14589
  const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
14563
14590
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
14564
- const token = await scopedToken2(parsed, deps, cfg, doFetch, out, `odla CLI (enroll ${name})`);
14591
+ const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
14592
+ const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
14593
+ const token = await scopedToken2(
14594
+ parsed,
14595
+ deps,
14596
+ cfg,
14597
+ doFetch,
14598
+ out,
14599
+ `odla CLI (enroll ${name})`,
14600
+ extended ? "platform:device:enroll:extended" : "app:device:enroll"
14601
+ );
14565
14602
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
14566
14603
  method: "POST",
14567
14604
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
@@ -14569,7 +14606,8 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14569
14606
  name,
14570
14607
  platform: import_node_process15.default.platform,
14571
14608
  appIds: apps,
14572
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
14609
+ ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
14610
+ ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
14573
14611
  })
14574
14612
  });
14575
14613
  const body = await response2.json().catch(() => ({}));
@@ -14622,12 +14660,12 @@ async function revoke(parsed, deps, cfg, doFetch, out, json) {
14622
14660
  out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
14623
14661
  if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
14624
14662
  }
14625
- async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
14663
+ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app:device:enroll") {
14626
14664
  const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
14627
14665
  const scopedTokenFile = credentials.scopedTokenFile;
14628
14666
  return getScopedPlatformToken({
14629
14667
  platform: cfg.platformUrl,
14630
- scope: "app:device:enroll",
14668
+ scope,
14631
14669
  email: stringOpt(parsed.options.email),
14632
14670
  label,
14633
14671
  fetch: doFetch,
@@ -14647,6 +14685,7 @@ var init_device_command = __esm({
14647
14685
  "src/device-command.ts"() {
14648
14686
  "use strict";
14649
14687
  init_cjs_shims();
14688
+ init_device_ttl();
14650
14689
  import_node_fs20 = require("fs");
14651
14690
  import_node_path18 = require("path");
14652
14691
  import_node_process15 = __toESM(require("process"), 1);