@odla-ai/cli 0.27.7 → 0.27.9

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/index.cjs CHANGED
@@ -337,17 +337,24 @@ function handshakeFile(cfg) {
337
337
  return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
338
338
  }
339
339
  var RESUME_MARGIN_MS = 5e3;
340
- function readPendingHandshake(path, platform, email) {
340
+ function readPendingHandshake(path, platform, email, requiredGrant) {
341
341
  const pending = readJsonFile(path);
342
342
  if (!pending || pending.platform !== platform || pending.email !== email) return null;
343
343
  if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
344
344
  if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
345
+ if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
345
346
  return {
346
347
  interval: typeof pending.interval === "number" ? pending.interval : 3,
347
348
  ...pending,
348
349
  approvalUrl: handshakeUrl(platform, pending.userCode)
349
350
  };
350
351
  }
352
+ function grantCovers(stored, required) {
353
+ if (required.optionalProjectCapabilities.length === 0) return true;
354
+ return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
355
+ (capability) => stored.optionalProjectCapabilities?.includes(capability)
356
+ );
357
+ }
351
358
  function writePendingHandshake(path, pending) {
352
359
  writePrivateJson(path, pending);
353
360
  }
@@ -378,7 +385,7 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
378
385
  }
379
386
 
380
387
  // src/token.ts
381
- async function getDeveloperToken(cfg, options, doFetch, out) {
388
+ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
382
389
  if (options.token) return options.token;
383
390
  const audience = platformAudience(cfg.platformUrl);
384
391
  if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
@@ -390,8 +397,10 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
390
397
  }
391
398
  return import_node_process4.default.env.ODLA_DEV_TOKEN;
392
399
  }
400
+ const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
401
+ const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
393
402
  const cached = readJsonFile(cfg.local.tokenFile);
394
- if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
403
+ if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
395
404
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
396
405
  return cached.token;
397
406
  }
@@ -402,17 +411,30 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
402
411
  out,
403
412
  audience,
404
413
  email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
405
- pendingFile: handshakeFile(cfg)
414
+ pendingFile: handshakeFile(cfg),
415
+ grantIntent
406
416
  };
407
417
  const waitMs = handshakeWaitMs(options.wait);
408
418
  const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
409
419
  clearPendingHandshake(ctx.pendingFile);
410
- writePrivateJson(cfg.local.tokenFile, { platform: audience, email: ctx.email, token, expiresAt });
420
+ writePrivateJson(cfg.local.tokenFile, {
421
+ platform: audience,
422
+ email: ctx.email,
423
+ projectIds: grantIntent.projectIds,
424
+ optionalProjectCapabilities,
425
+ token,
426
+ expiresAt
427
+ });
411
428
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
412
429
  return token;
413
430
  }
414
431
  async function resumePendingHandshake(ctx, waitMs) {
415
- const pending = readPendingHandshake(ctx.pendingFile, ctx.audience, ctx.email);
432
+ const pending = readPendingHandshake(
433
+ ctx.pendingFile,
434
+ ctx.audience,
435
+ ctx.email,
436
+ ctx.grantIntent
437
+ );
416
438
  if (!pending) return null;
417
439
  ctx.out.error("");
418
440
  ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
@@ -457,6 +479,7 @@ async function freshHandshake(ctx, waitMs) {
457
479
  label: `${ctx.cfg.app.id} provisioner`,
458
480
  agentHandle: projectAgentHandle(ctx.cfg.app.id),
459
481
  projectIds: [ctx.cfg.app.id],
482
+ optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities,
460
483
  fetch: ctx.doFetch,
461
484
  waitMs,
462
485
  onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
@@ -468,7 +491,9 @@ async function freshHandshake(ctx, waitMs) {
468
491
  deviceCode,
469
492
  approvalUrl,
470
493
  interval,
471
- expiresAt: Date.now() + expiresIn * 1e3
494
+ expiresAt: Date.now() + expiresIn * 1e3,
495
+ projectIds: ctx.grantIntent.projectIds,
496
+ optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
472
497
  };
473
498
  writePendingHandshake(ctx.pendingFile, started);
474
499
  await presentHandshakeApproval(ctx.out, {
@@ -491,6 +516,12 @@ async function freshHandshake(ctx, waitMs) {
491
516
  stopReminder?.();
492
517
  }
493
518
  }
519
+ function cachedGrantCovers(cached, required) {
520
+ if (required.optionalProjectCapabilities.length === 0) return true;
521
+ return required.projectIds.every((id) => cached.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
522
+ (capability) => cached.optionalProjectCapabilities?.includes(capability)
523
+ );
524
+ }
494
525
  function projectAgentHandle(appId) {
495
526
  const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
496
527
  if (candidate.length <= 32) return candidate;
@@ -3154,7 +3185,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
3154
3185
  if (res.ok || res.status === 404) return;
3155
3186
  if (res.status === 403) {
3156
3187
  throw new Error(
3157
- `${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
3188
+ `${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; re-run provision with a fresh owner-approved provision handshake. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
3158
3189
  );
3159
3190
  }
3160
3191
  throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
@@ -10991,7 +11022,9 @@ async function provision(options) {
10991
11022
  out.log("secrets: Wrangler config and login preflight passed");
10992
11023
  }
10993
11024
  const doFetch = options.fetch ?? fetch;
10994
- const token = await getDeveloperToken(cfg, options, doFetch, out);
11025
+ const token = await getDeveloperToken(cfg, options, doFetch, out, {
11026
+ optionalProjectCapabilities: ["app.manage"]
11027
+ });
10995
11028
  const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
10996
11029
  const existing = await apps.resolveApp(cfg.app.id);
10997
11030
  if (existing) {