@odla-ai/cli 0.27.8 → 0.27.10
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 +7 -1
- package/dist/bin.cjs +59 -12
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-MZSU4YQL.js → chunk-OGZELVS5.js} +60 -13
- package/dist/chunk-OGZELVS5.js.map +1 -0
- package/dist/index.cjs +59 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +1 -1
- package/skills/odla/references/agent-identity.md +5 -4
- package/skills/odla/references/build.md +7 -6
- package/skills/odla/references/co-owners.md +11 -13
- package/dist/chunk-MZSU4YQL.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -243,17 +243,24 @@ function handshakeFile(cfg) {
|
|
|
243
243
|
return join(dirname2(cfg.local.tokenFile), "handshake.local.json");
|
|
244
244
|
}
|
|
245
245
|
var RESUME_MARGIN_MS = 5e3;
|
|
246
|
-
function readPendingHandshake(path, platform, email) {
|
|
246
|
+
function readPendingHandshake(path, platform, email, requiredGrant) {
|
|
247
247
|
const pending = readJsonFile(path);
|
|
248
248
|
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
249
249
|
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
|
|
250
250
|
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
251
|
+
if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
|
|
251
252
|
return {
|
|
252
253
|
interval: typeof pending.interval === "number" ? pending.interval : 3,
|
|
253
254
|
...pending,
|
|
254
255
|
approvalUrl: handshakeUrl(platform, pending.userCode)
|
|
255
256
|
};
|
|
256
257
|
}
|
|
258
|
+
function grantCovers(stored, required) {
|
|
259
|
+
if (required.optionalProjectCapabilities.length === 0) return true;
|
|
260
|
+
return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
|
|
261
|
+
(capability) => stored.optionalProjectCapabilities?.includes(capability)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
257
264
|
function writePendingHandshake(path, pending) {
|
|
258
265
|
writePrivateJson(path, pending);
|
|
259
266
|
}
|
|
@@ -284,7 +291,7 @@ function handshakeWaitMs(waitSeconds, interactive = process4.stdout.isTTY === tr
|
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
// src/token.ts
|
|
287
|
-
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
294
|
+
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
288
295
|
if (options.token) return options.token;
|
|
289
296
|
const audience = platformAudience(cfg.platformUrl);
|
|
290
297
|
if (process5.env.ODLA_DEV_TOKEN) {
|
|
@@ -296,8 +303,10 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
|
296
303
|
}
|
|
297
304
|
return process5.env.ODLA_DEV_TOKEN;
|
|
298
305
|
}
|
|
306
|
+
const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
|
|
307
|
+
const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
|
|
299
308
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
300
|
-
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
309
|
+
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
|
|
301
310
|
out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
302
311
|
return cached.token;
|
|
303
312
|
}
|
|
@@ -308,17 +317,30 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
|
308
317
|
out,
|
|
309
318
|
audience,
|
|
310
319
|
email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
|
|
311
|
-
pendingFile: handshakeFile(cfg)
|
|
320
|
+
pendingFile: handshakeFile(cfg),
|
|
321
|
+
grantIntent
|
|
312
322
|
};
|
|
313
323
|
const waitMs = handshakeWaitMs(options.wait);
|
|
314
324
|
const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
|
|
315
325
|
clearPendingHandshake(ctx.pendingFile);
|
|
316
|
-
writePrivateJson(cfg.local.tokenFile, {
|
|
326
|
+
writePrivateJson(cfg.local.tokenFile, {
|
|
327
|
+
platform: audience,
|
|
328
|
+
email: ctx.email,
|
|
329
|
+
projectIds: grantIntent.projectIds,
|
|
330
|
+
optionalProjectCapabilities,
|
|
331
|
+
token,
|
|
332
|
+
expiresAt
|
|
333
|
+
});
|
|
317
334
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
318
335
|
return token;
|
|
319
336
|
}
|
|
320
337
|
async function resumePendingHandshake(ctx, waitMs) {
|
|
321
|
-
const pending = readPendingHandshake(
|
|
338
|
+
const pending = readPendingHandshake(
|
|
339
|
+
ctx.pendingFile,
|
|
340
|
+
ctx.audience,
|
|
341
|
+
ctx.email,
|
|
342
|
+
ctx.grantIntent
|
|
343
|
+
);
|
|
322
344
|
if (!pending) return null;
|
|
323
345
|
ctx.out.error("");
|
|
324
346
|
ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
@@ -363,6 +385,7 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
363
385
|
label: `${ctx.cfg.app.id} provisioner`,
|
|
364
386
|
agentHandle: projectAgentHandle(ctx.cfg.app.id),
|
|
365
387
|
projectIds: [ctx.cfg.app.id],
|
|
388
|
+
optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities,
|
|
366
389
|
fetch: ctx.doFetch,
|
|
367
390
|
waitMs,
|
|
368
391
|
onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
|
|
@@ -374,7 +397,9 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
374
397
|
deviceCode,
|
|
375
398
|
approvalUrl,
|
|
376
399
|
interval,
|
|
377
|
-
expiresAt: Date.now() + expiresIn * 1e3
|
|
400
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
401
|
+
projectIds: ctx.grantIntent.projectIds,
|
|
402
|
+
optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
|
|
378
403
|
};
|
|
379
404
|
writePendingHandshake(ctx.pendingFile, started);
|
|
380
405
|
await presentHandshakeApproval(ctx.out, {
|
|
@@ -397,6 +422,12 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
397
422
|
stopReminder?.();
|
|
398
423
|
}
|
|
399
424
|
}
|
|
425
|
+
function cachedGrantCovers(cached, required) {
|
|
426
|
+
if (required.optionalProjectCapabilities.length === 0) return true;
|
|
427
|
+
return required.projectIds.every((id) => cached.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
|
|
428
|
+
(capability) => cached.optionalProjectCapabilities?.includes(capability)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
400
431
|
function projectAgentHandle(appId) {
|
|
401
432
|
const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
|
|
402
433
|
if (candidate.length <= 32) return candidate;
|
|
@@ -1366,7 +1397,7 @@ function parseCalendarStatus(raw, env) {
|
|
|
1366
1397
|
throw new Error("calendar status returned unsupported access");
|
|
1367
1398
|
}
|
|
1368
1399
|
const errorValue = record(value2.error) ?? record(connection.error);
|
|
1369
|
-
const
|
|
1400
|
+
const errorCode2 = textField(value2.lastErrorCode, 128);
|
|
1370
1401
|
const bookingPageValue = Object.hasOwn(value2, "bookingPageUrl") ? value2.bookingPageUrl : Object.hasOwn(config, "bookingPageUrl") ? config.bookingPageUrl : googleConfig.bookingPageUrl;
|
|
1371
1402
|
const connected = typeof (value2.connected ?? connection.connected) === "boolean" ? Boolean(value2.connected ?? connection.connected) : ["healthy", "degraded"].includes(stateValue);
|
|
1372
1403
|
const bookingCalendarId = textField(value2.bookingCalendarId ?? config.bookingCalendarId, 1024);
|
|
@@ -1385,10 +1416,10 @@ function parseCalendarStatus(raw, env) {
|
|
|
1385
1416
|
...optionalNullableUrl("bookingPageUrl", bookingPageValue),
|
|
1386
1417
|
grantedScopes: stringList(value2.grantedScopes ?? connection.grantedScopes ?? connection.scopes),
|
|
1387
1418
|
...optionalText("attemptId", value2.attemptId ?? connection.attemptId, 180),
|
|
1388
|
-
...errorValue ||
|
|
1419
|
+
...errorValue || errorCode2 ? { error: {
|
|
1389
1420
|
...textField(errorValue?.code, 128) ? { code: textField(errorValue?.code, 128) } : {},
|
|
1390
1421
|
...textField(errorValue?.message, 500) ? { message: textField(errorValue?.message, 500) } : {},
|
|
1391
|
-
...!errorValue &&
|
|
1422
|
+
...!errorValue && errorCode2 ? { code: errorCode2 } : {}
|
|
1392
1423
|
} } : {}
|
|
1393
1424
|
};
|
|
1394
1425
|
}
|
|
@@ -1768,12 +1799,26 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
1768
1799
|
});
|
|
1769
1800
|
if (res.ok || res.status === 404) return;
|
|
1770
1801
|
if (res.status === 403) {
|
|
1802
|
+
const detail = await safeText4(res);
|
|
1803
|
+
if (errorCode(detail) === "human_session_required") {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`${env}: odla-db rejected the provision credential before checking ownership for "${cfg.app.id}" (tenant ${tenantId}): human_session_required. Retrying or changing app owners will not help; the deployed odla-db must accept owner-approved app.manage credentials on provisioning routes`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1771
1808
|
throw new Error(
|
|
1772
|
-
`${env}:
|
|
1809
|
+
`${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`
|
|
1773
1810
|
);
|
|
1774
1811
|
}
|
|
1775
1812
|
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
|
|
1776
1813
|
}
|
|
1814
|
+
function errorCode(text2) {
|
|
1815
|
+
try {
|
|
1816
|
+
const body = JSON.parse(text2);
|
|
1817
|
+
return typeof body.error?.code === "string" ? body.error.code : null;
|
|
1818
|
+
} catch {
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1777
1822
|
async function postJson(doFetch, url, bearer, body) {
|
|
1778
1823
|
const res = await doFetch(url, {
|
|
1779
1824
|
method: "POST",
|
|
@@ -7404,7 +7449,9 @@ async function provision(options) {
|
|
|
7404
7449
|
out.log("secrets: Wrangler config and login preflight passed");
|
|
7405
7450
|
}
|
|
7406
7451
|
const doFetch = options.fetch ?? fetch;
|
|
7407
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
7452
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
7453
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
7454
|
+
});
|
|
7408
7455
|
const apps = createAppsClient3({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
7409
7456
|
const existing = await apps.resolveApp(cfg.app.id);
|
|
7410
7457
|
if (existing) {
|
|
@@ -13166,4 +13213,4 @@ export {
|
|
|
13166
13213
|
exitCodeFor,
|
|
13167
13214
|
runCli
|
|
13168
13215
|
};
|
|
13169
|
-
//# sourceMappingURL=chunk-
|
|
13216
|
+
//# sourceMappingURL=chunk-OGZELVS5.js.map
|