@odla-ai/cli 0.29.0 → 0.30.1
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 +61 -29
- package/dist/bin.cjs +350 -191
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-YTVLTADT.js → chunk-7V7WVTDT.js} +304 -166
- package/dist/chunk-7V7WVTDT.js.map +1 -0
- package/dist/{cli-DNOAYDBR.js → cli-SZUWA6EF.js} +2 -2
- package/dist/index.cjs +317 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +1 -1
- package/dist/runtime/pi-agent.js +18728 -15
- package/package.json +1 -1
- package/skills/odla/SKILL.md +10 -5
- package/skills/odla/references/pm-work-intake.md +5 -3
- package/skills/odla/references/pm.md +4 -0
- package/skills/odla-migrate/references/secrets-map.md +22 -22
- package/skills/odla-migrate/references/troubleshooting.md +14 -16
- package/dist/chunk-YTVLTADT.js.map +0 -1
- /package/dist/{cli-DNOAYDBR.js.map → cli-SZUWA6EF.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -203,17 +203,45 @@ function handshakeUrl(platformUrl, userCode) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// src/handshake-state.ts
|
|
206
|
-
var
|
|
207
|
-
var
|
|
206
|
+
var import_node_fs = require("fs");
|
|
207
|
+
var import_node_path = require("path");
|
|
208
208
|
var import_node_process3 = __toESM(require("process"), 1);
|
|
209
|
+
function handshakeFile(cfg) {
|
|
210
|
+
return (0, import_node_path.join)((0, import_node_path.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
211
|
+
}
|
|
212
|
+
function clearPendingHandshake(path) {
|
|
213
|
+
(0, import_node_fs.rmSync)(path, { force: true });
|
|
214
|
+
}
|
|
215
|
+
function minutesLeft(expiresAt) {
|
|
216
|
+
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
217
|
+
}
|
|
218
|
+
function approvalHint(pending) {
|
|
219
|
+
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
220
|
+
}
|
|
221
|
+
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
222
|
+
const timer = setInterval(() => {
|
|
223
|
+
for (const line of reminderLines({
|
|
224
|
+
userCode: pending.userCode,
|
|
225
|
+
approvalUrl: pending.approvalUrl,
|
|
226
|
+
minutesLeft: minutesLeft(pending.expiresAt)
|
|
227
|
+
}))
|
|
228
|
+
out.log(line);
|
|
229
|
+
}, periodMs);
|
|
230
|
+
timer.unref?.();
|
|
231
|
+
return () => clearInterval(timer);
|
|
232
|
+
}
|
|
233
|
+
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
234
|
+
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
235
|
+
return interactive ? void 0 : 9e4;
|
|
236
|
+
}
|
|
209
237
|
|
|
210
238
|
// src/local.ts
|
|
211
|
-
var
|
|
212
|
-
var
|
|
239
|
+
var import_node_fs2 = require("fs");
|
|
240
|
+
var import_node_path2 = require("path");
|
|
213
241
|
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
214
242
|
function readJsonFile(path) {
|
|
215
243
|
try {
|
|
216
|
-
return JSON.parse((0,
|
|
244
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
217
245
|
} catch {
|
|
218
246
|
return null;
|
|
219
247
|
}
|
|
@@ -223,10 +251,10 @@ function writePrivateJson(path, value2) {
|
|
|
223
251
|
`);
|
|
224
252
|
}
|
|
225
253
|
function readCredentials(path) {
|
|
226
|
-
if (!(0,
|
|
254
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
227
255
|
let value2;
|
|
228
256
|
try {
|
|
229
|
-
value2 = JSON.parse((0,
|
|
257
|
+
value2 = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
230
258
|
} catch {
|
|
231
259
|
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
232
260
|
}
|
|
@@ -256,14 +284,14 @@ function mergeCredential(current, update) {
|
|
|
256
284
|
return next;
|
|
257
285
|
}
|
|
258
286
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
259
|
-
const path = (0,
|
|
260
|
-
const existing = (0,
|
|
287
|
+
const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
|
|
288
|
+
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
261
289
|
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
262
290
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
263
291
|
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
264
292
|
if (missing.length === 0) return;
|
|
265
293
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
266
|
-
(0,
|
|
294
|
+
(0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
267
295
|
`);
|
|
268
296
|
}
|
|
269
297
|
function o11yDevVars(cfg) {
|
|
@@ -277,7 +305,7 @@ function o11yDevVars(cfg) {
|
|
|
277
305
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
278
306
|
if (!requested) return null;
|
|
279
307
|
if (requested === true) return cfg.local.devVarsFile;
|
|
280
|
-
return (0,
|
|
308
|
+
return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
|
|
281
309
|
}
|
|
282
310
|
function writeDevVars(path, credentials, env, o11y) {
|
|
283
311
|
const entry = credentials.envs[env];
|
|
@@ -291,7 +319,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
291
319
|
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
292
320
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
293
321
|
}
|
|
294
|
-
const existing = (0,
|
|
322
|
+
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
295
323
|
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
296
324
|
while (retained.at(-1) === "") retained.pop();
|
|
297
325
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
@@ -317,74 +345,22 @@ function isManagedDevVar(line) {
|
|
|
317
345
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
318
346
|
}
|
|
319
347
|
function writePrivateText(path, text2) {
|
|
320
|
-
(0,
|
|
348
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
321
349
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
322
|
-
(0,
|
|
323
|
-
(0,
|
|
324
|
-
(0,
|
|
350
|
+
(0, import_node_fs2.writeFileSync)(temporary, text2, { mode: 384 });
|
|
351
|
+
(0, import_node_fs2.chmodSync)(temporary, 384);
|
|
352
|
+
(0, import_node_fs2.renameSync)(temporary, path);
|
|
325
353
|
}
|
|
326
354
|
function gitignoreEntry(rootDir, path) {
|
|
327
|
-
const rel = (0,
|
|
328
|
-
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0,
|
|
355
|
+
const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
|
|
356
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
|
|
329
357
|
return rel.replaceAll("\\", "/");
|
|
330
358
|
}
|
|
331
359
|
function displayPath(path, rootDir = process.cwd()) {
|
|
332
|
-
const rel = (0,
|
|
360
|
+
const rel = (0, import_node_path2.relative)(rootDir, path);
|
|
333
361
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
334
362
|
}
|
|
335
363
|
|
|
336
|
-
// src/handshake-state.ts
|
|
337
|
-
function handshakeFile(cfg) {
|
|
338
|
-
return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
339
|
-
}
|
|
340
|
-
var RESUME_MARGIN_MS = 5e3;
|
|
341
|
-
function readPendingHandshake(path, platform, email, requiredGrant) {
|
|
342
|
-
const pending = readJsonFile(path);
|
|
343
|
-
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
344
|
-
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
|
|
345
|
-
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
346
|
-
if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
|
|
347
|
-
return {
|
|
348
|
-
interval: typeof pending.interval === "number" ? pending.interval : 3,
|
|
349
|
-
...pending,
|
|
350
|
-
approvalUrl: handshakeUrl(platform, pending.userCode)
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
function grantCovers(stored, required) {
|
|
354
|
-
if (required.optionalProjectCapabilities.length === 0) return true;
|
|
355
|
-
return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
|
|
356
|
-
(capability) => stored.optionalProjectCapabilities?.includes(capability)
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
function writePendingHandshake(path, pending) {
|
|
360
|
-
writePrivateJson(path, pending);
|
|
361
|
-
}
|
|
362
|
-
function clearPendingHandshake(path) {
|
|
363
|
-
(0, import_node_fs2.rmSync)(path, { force: true });
|
|
364
|
-
}
|
|
365
|
-
function minutesLeft(expiresAt) {
|
|
366
|
-
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
367
|
-
}
|
|
368
|
-
function approvalHint(pending) {
|
|
369
|
-
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
370
|
-
}
|
|
371
|
-
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
372
|
-
const timer = setInterval(() => {
|
|
373
|
-
for (const line of reminderLines({
|
|
374
|
-
userCode: pending.userCode,
|
|
375
|
-
approvalUrl: pending.approvalUrl,
|
|
376
|
-
minutesLeft: minutesLeft(pending.expiresAt)
|
|
377
|
-
}))
|
|
378
|
-
out.log(line);
|
|
379
|
-
}, periodMs);
|
|
380
|
-
timer.unref?.();
|
|
381
|
-
return () => clearInterval(timer);
|
|
382
|
-
}
|
|
383
|
-
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
384
|
-
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
385
|
-
return interactive ? void 0 : 9e4;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
364
|
// src/token.ts
|
|
389
365
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
390
366
|
const audience = platformAudience(cfg.platformUrl);
|
|
@@ -423,8 +399,8 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
423
399
|
grantIntent
|
|
424
400
|
};
|
|
425
401
|
const waitMs = handshakeWaitMs(options.wait);
|
|
426
|
-
|
|
427
|
-
const { token, expiresAt } = await
|
|
402
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
403
|
+
const { token, expiresAt } = await freshHandshake(ctx, waitMs);
|
|
428
404
|
clearPendingHandshake(ctx.pendingFile);
|
|
429
405
|
writePrivateJson(cfg.local.tokenFile, {
|
|
430
406
|
platform: audience,
|
|
@@ -437,47 +413,6 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
437
413
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
438
414
|
return token;
|
|
439
415
|
}
|
|
440
|
-
async function resumePendingHandshake(ctx, waitMs) {
|
|
441
|
-
const pending = readPendingHandshake(
|
|
442
|
-
ctx.pendingFile,
|
|
443
|
-
ctx.audience,
|
|
444
|
-
ctx.email,
|
|
445
|
-
ctx.grantIntent
|
|
446
|
-
);
|
|
447
|
-
if (!pending) return null;
|
|
448
|
-
ctx.out.error("");
|
|
449
|
-
ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
450
|
-
await presentHandshakeApproval(ctx.out, {
|
|
451
|
-
userCode: pending.userCode,
|
|
452
|
-
approvalUrl: pending.approvalUrl,
|
|
453
|
-
minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
|
|
454
|
-
purpose: `sign this terminal in as ${ctx.email}`
|
|
455
|
-
}, ctx.options);
|
|
456
|
-
ctx.out.error("");
|
|
457
|
-
const stopReminder = approvalReminder(ctx.out, pending);
|
|
458
|
-
try {
|
|
459
|
-
return await (0, import_db.collectToken)({
|
|
460
|
-
endpoint: ctx.cfg.platformUrl,
|
|
461
|
-
deviceCode: pending.deviceCode,
|
|
462
|
-
expiresAt: pending.expiresAt,
|
|
463
|
-
interval: pending.interval,
|
|
464
|
-
waitMs,
|
|
465
|
-
fetch: ctx.doFetch
|
|
466
|
-
});
|
|
467
|
-
} catch (err) {
|
|
468
|
-
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
469
|
-
if (code === "handshake_pending") throw stillPending(pending, ctx.email);
|
|
470
|
-
if (code === "handshake_expired" || code === "handshake_timeout") {
|
|
471
|
-
clearPendingHandshake(ctx.pendingFile);
|
|
472
|
-
ctx.out.error("auth: pending handshake lapsed unapproved; starting a fresh one");
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
if (code === "handshake_denied") clearPendingHandshake(ctx.pendingFile);
|
|
476
|
-
throw err;
|
|
477
|
-
} finally {
|
|
478
|
-
stopReminder();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
416
|
async function freshHandshake(ctx, waitMs) {
|
|
482
417
|
let started;
|
|
483
418
|
let stopReminder;
|
|
@@ -505,7 +440,6 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
505
440
|
projectIds: ctx.grantIntent.projectIds,
|
|
506
441
|
optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
|
|
507
442
|
};
|
|
508
|
-
writePendingHandshake(ctx.pendingFile, started);
|
|
509
443
|
await presentHandshakeApproval(ctx.out, {
|
|
510
444
|
userCode,
|
|
511
445
|
approvalUrl,
|
|
@@ -541,7 +475,7 @@ function projectAgentHandle(appId) {
|
|
|
541
475
|
function stillPending(pending, email) {
|
|
542
476
|
return new import_db.OdlaError(
|
|
543
477
|
"handshake_pending",
|
|
544
|
-
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}
|
|
478
|
+
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}; if this command exits, re-run it to request a new code`,
|
|
545
479
|
{ retryable: true }
|
|
546
480
|
);
|
|
547
481
|
}
|
|
@@ -3961,6 +3895,41 @@ async function wranglerLoggedIn(run, cwd) {
|
|
|
3961
3895
|
return false;
|
|
3962
3896
|
}
|
|
3963
3897
|
}
|
|
3898
|
+
async function wranglerRuntimeTarget(run, opts) {
|
|
3899
|
+
const configPath = findWranglerConfig(opts.cwd);
|
|
3900
|
+
if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
|
|
3901
|
+
const config = readWranglerConfig(configPath);
|
|
3902
|
+
if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
|
|
3903
|
+
const baseName = typeof config.name === "string" ? config.name : "";
|
|
3904
|
+
const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
|
|
3905
|
+
const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
|
|
3906
|
+
const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
|
|
3907
|
+
const serviceEnvironments = config.legacy_env === false;
|
|
3908
|
+
if (serviceEnvironments && explicitName) {
|
|
3909
|
+
throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
|
|
3910
|
+
}
|
|
3911
|
+
const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
|
|
3912
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
|
|
3913
|
+
throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
|
|
3914
|
+
}
|
|
3915
|
+
const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
|
|
3916
|
+
const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
|
|
3917
|
+
if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
|
|
3918
|
+
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
3919
|
+
}
|
|
3920
|
+
const discovered = [...new Set(`${whoami.stdout}
|
|
3921
|
+
${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
|
|
3922
|
+
const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
|
|
3923
|
+
if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
|
|
3924
|
+
throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
|
|
3925
|
+
}
|
|
3926
|
+
return {
|
|
3927
|
+
provider: "cloudflare",
|
|
3928
|
+
accountId,
|
|
3929
|
+
scriptName,
|
|
3930
|
+
...opts.env ? { environment: opts.env } : {}
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3964
3933
|
function wranglerPutSecret(run, opts) {
|
|
3965
3934
|
const args = [
|
|
3966
3935
|
"wrangler",
|
|
@@ -3972,6 +3941,16 @@ function wranglerPutSecret(run, opts) {
|
|
|
3972
3941
|
];
|
|
3973
3942
|
return run("npx", args, { input: opts.value, cwd: opts.cwd });
|
|
3974
3943
|
}
|
|
3944
|
+
function wranglerBulkSecrets(run, opts) {
|
|
3945
|
+
const args = [
|
|
3946
|
+
"wrangler",
|
|
3947
|
+
"secret",
|
|
3948
|
+
"bulk",
|
|
3949
|
+
...opts.env ? ["--env", opts.env] : [],
|
|
3950
|
+
...opts.configPath ? ["--config", opts.configPath] : []
|
|
3951
|
+
];
|
|
3952
|
+
return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
|
|
3953
|
+
}
|
|
3975
3954
|
|
|
3976
3955
|
// src/doctor-checks.ts
|
|
3977
3956
|
function lintRules(rules, entities, publicRead) {
|
|
@@ -4460,9 +4439,6 @@ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
|
4460
4439
|
async function secretsPush(options) {
|
|
4461
4440
|
await secretsPushImpl(options, true);
|
|
4462
4441
|
}
|
|
4463
|
-
async function secretsPushAfterPreflight(options) {
|
|
4464
|
-
await secretsPushImpl(options, false);
|
|
4465
|
-
}
|
|
4466
4442
|
async function secretsPushImpl(options, preflight) {
|
|
4467
4443
|
const out = options.stdout ?? console;
|
|
4468
4444
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -4869,28 +4845,31 @@ async function smoke(options) {
|
|
|
4869
4845
|
const out = options.stdout ?? console;
|
|
4870
4846
|
const cfg = await loadProjectConfig(options.configPath);
|
|
4871
4847
|
const env = resolveEnv(cfg, options.env);
|
|
4872
|
-
const
|
|
4873
|
-
|
|
4848
|
+
const runtime = options.runtime === true;
|
|
4849
|
+
const credentials = runtime ? null : readCredentials(cfg.local.credentialsFile);
|
|
4850
|
+
if (!runtime && !credentials) {
|
|
4874
4851
|
throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
|
|
4875
4852
|
}
|
|
4876
|
-
if (credentials.appId !== cfg.app.id) {
|
|
4853
|
+
if (credentials && credentials.appId !== cfg.app.id) {
|
|
4877
4854
|
throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
|
|
4878
4855
|
}
|
|
4879
|
-
const entry = credentials
|
|
4880
|
-
if (!entry?.tenantId) {
|
|
4856
|
+
const entry = credentials?.envs[env];
|
|
4857
|
+
if (!runtime && !entry?.tenantId) {
|
|
4881
4858
|
throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
4882
4859
|
}
|
|
4883
4860
|
const hasDb = cfg.services.includes("db");
|
|
4884
4861
|
const hasO11y = cfg.services.includes("o11y");
|
|
4885
|
-
if (hasDb && !entry
|
|
4862
|
+
if (!runtime && hasDb && !entry?.dbKey) {
|
|
4886
4863
|
throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
4887
4864
|
}
|
|
4888
|
-
if (hasO11y && !entry
|
|
4865
|
+
if (!runtime && hasO11y && !entry?.o11yToken) {
|
|
4889
4866
|
throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
4890
4867
|
}
|
|
4891
4868
|
const doFetch = options.fetch ?? fetch;
|
|
4869
|
+
const tenantId = entry?.tenantId ?? resolveTenant(cfg, env).tenant;
|
|
4892
4870
|
out.log(`smoke: ${cfg.app.id}/${env}`);
|
|
4893
|
-
out.log(`
|
|
4871
|
+
out.log(` mode: ${runtime ? "runtime (Worker-held credentials)" : "local credentials"}`);
|
|
4872
|
+
out.log(` tenant: ${tenantId}`);
|
|
4894
4873
|
const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
|
|
4895
4874
|
out.log(` public-config: ok`);
|
|
4896
4875
|
if (cfg.services.includes("ai") && cfg.ai?.provider) {
|
|
@@ -4904,7 +4883,7 @@ async function smoke(options) {
|
|
|
4904
4883
|
if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
|
|
4905
4884
|
out.log(" ai: hosted");
|
|
4906
4885
|
}
|
|
4907
|
-
if (hasO11y) out.log(` o11y: credentials present`);
|
|
4886
|
+
if (hasO11y) out.log(runtime ? ` o11y: Worker-held credential` : ` o11y: credentials present`);
|
|
4908
4887
|
if (cfg.services.includes("calendar")) {
|
|
4909
4888
|
const token = await getDeveloperToken(
|
|
4910
4889
|
cfg,
|
|
@@ -4923,10 +4902,10 @@ async function smoke(options) {
|
|
|
4923
4902
|
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
4924
4903
|
}
|
|
4925
4904
|
const database = await resolveDatabaseConfig(cfg);
|
|
4926
|
-
if (hasDb) {
|
|
4905
|
+
if (hasDb && !runtime) {
|
|
4927
4906
|
const expectedSchema = database.schema;
|
|
4928
4907
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
4929
|
-
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(
|
|
4908
|
+
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, entry.dbKey);
|
|
4930
4909
|
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
4931
4910
|
const liveEntities = serializedEntities(liveSchema);
|
|
4932
4911
|
if (expectedEntities.length) {
|
|
@@ -4936,7 +4915,7 @@ async function smoke(options) {
|
|
|
4936
4915
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
4937
4916
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
4938
4917
|
if (aggregateEntity) {
|
|
4939
|
-
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(
|
|
4918
|
+
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/aggregate`, entry.dbKey, {
|
|
4940
4919
|
ns: aggregateEntity,
|
|
4941
4920
|
aggregate: { count: true }
|
|
4942
4921
|
});
|
|
@@ -4945,9 +4924,20 @@ async function smoke(options) {
|
|
|
4945
4924
|
} else {
|
|
4946
4925
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
4947
4926
|
}
|
|
4927
|
+
} else if (hasDb) {
|
|
4928
|
+
out.log(` db: Worker-held credential (direct schema/aggregate skipped)`);
|
|
4948
4929
|
} else {
|
|
4949
4930
|
out.log(` db: skipped (not enabled)`);
|
|
4950
4931
|
}
|
|
4932
|
+
if (runtime) {
|
|
4933
|
+
const link = cfg.links?.[env];
|
|
4934
|
+
if (!link) throw new Error(`runtime smoke requires links.${env} in odla.config.mjs`);
|
|
4935
|
+
const res = await doFetch(link, { redirect: "manual" });
|
|
4936
|
+
if (res.status < 200 || res.status >= 500) {
|
|
4937
|
+
throw new Error(`runtime target ${new URL(link).origin} returned ${res.status}`);
|
|
4938
|
+
}
|
|
4939
|
+
out.log(` runtime target: ${res.status}`);
|
|
4940
|
+
}
|
|
4951
4941
|
const probes = database.integrations.flatMap(
|
|
4952
4942
|
(integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
|
|
4953
4943
|
);
|
|
@@ -5150,10 +5140,11 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5150
5140
|
return true;
|
|
5151
5141
|
}
|
|
5152
5142
|
if (command === "smoke") {
|
|
5153
|
-
assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
|
|
5143
|
+
assertArgs(parsed, ["config", "env", "runtime", "token", "email", "open"], 1);
|
|
5154
5144
|
await smoke({
|
|
5155
5145
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
5156
5146
|
env: stringOpt(parsed.options.env),
|
|
5147
|
+
runtime: parsed.options.runtime === true,
|
|
5157
5148
|
token: stringOpt(parsed.options.token),
|
|
5158
5149
|
email: stringOpt(parsed.options.email),
|
|
5159
5150
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
@@ -8827,6 +8818,55 @@ function requireName(parsed) {
|
|
|
8827
8818
|
return name;
|
|
8828
8819
|
}
|
|
8829
8820
|
|
|
8821
|
+
// src/credential-command.ts
|
|
8822
|
+
async function responseError(response2) {
|
|
8823
|
+
return redactSecrets((await response2.text()).slice(0, 1e3));
|
|
8824
|
+
}
|
|
8825
|
+
async function credentialCommand(parsed, deps = {}) {
|
|
8826
|
+
const action2 = parsed.positionals[1] ?? "list";
|
|
8827
|
+
if (action2 !== "list" && action2 !== "revoke") {
|
|
8828
|
+
throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
|
|
8829
|
+
}
|
|
8830
|
+
assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
|
|
8831
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
8832
|
+
const doFetch = deps.fetch ?? fetch;
|
|
8833
|
+
const out = deps.stdout ?? console;
|
|
8834
|
+
const token = await getDeveloperToken(cfg, {
|
|
8835
|
+
configPath: cfg.configPath,
|
|
8836
|
+
token: stringOpt(parsed.options.token),
|
|
8837
|
+
email: stringOpt(parsed.options.email),
|
|
8838
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
8839
|
+
openApprovalUrl: deps.openUrl
|
|
8840
|
+
}, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
|
|
8841
|
+
const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
|
|
8842
|
+
if (action2 === "revoke") {
|
|
8843
|
+
const id = parsed.positionals[2];
|
|
8844
|
+
if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
|
|
8845
|
+
const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
|
|
8846
|
+
method: "DELETE",
|
|
8847
|
+
headers: { authorization: `Bearer ${token}` }
|
|
8848
|
+
});
|
|
8849
|
+
if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
|
|
8850
|
+
const body2 = await response3.json();
|
|
8851
|
+
return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
|
|
8852
|
+
}
|
|
8853
|
+
const query = new URLSearchParams();
|
|
8854
|
+
const requestedEnv = stringOpt(parsed.options.env);
|
|
8855
|
+
if (requestedEnv) query.set("env", requestedEnv);
|
|
8856
|
+
const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
|
|
8857
|
+
headers: { authorization: `Bearer ${token}` }
|
|
8858
|
+
});
|
|
8859
|
+
if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
|
|
8860
|
+
const body = await response2.json();
|
|
8861
|
+
const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
|
|
8862
|
+
if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
|
|
8863
|
+
if (!credentials.length) return out.log("no active runtime credential receipts");
|
|
8864
|
+
out.log("RECEIPT ENV TARGET CREATED");
|
|
8865
|
+
for (const item of credentials) {
|
|
8866
|
+
out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
|
|
8867
|
+
}
|
|
8868
|
+
}
|
|
8869
|
+
|
|
8830
8870
|
// src/help-usage.ts
|
|
8831
8871
|
var USAGE_SECTION = `
|
|
8832
8872
|
Start here:
|
|
@@ -8945,7 +8985,9 @@ Usage:
|
|
|
8945
8985
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
8946
8986
|
odla-ai security run [target] --self --ack-redacted-source
|
|
8947
8987
|
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]
|
|
8948
|
-
odla-ai
|
|
8988
|
+
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
8989
|
+
odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
|
|
8990
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
|
|
8949
8991
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8950
8992
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
8951
8993
|
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
@@ -9047,7 +9089,7 @@ Commands:
|
|
|
9047
9089
|
"provision --live --yes" initializes only the live instance of
|
|
9048
9090
|
an existing sandbox app and enables every configured service;
|
|
9049
9091
|
no edit to the dev-first envs list is required.
|
|
9050
|
-
smoke Verify
|
|
9092
|
+
smoke Verify local service access, or use --runtime for a Worker-held credential deployment.
|
|
9051
9093
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
9052
9094
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
9053
9095
|
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
@@ -9067,9 +9109,13 @@ Safety:
|
|
|
9067
9109
|
pinned versions of every external @odla-ai runtime module before importing
|
|
9068
9110
|
the command graph. A stale workspace module blocks with its resolved path and
|
|
9069
9111
|
tells the agent to update/rebase, npm ci, and rebuild.
|
|
9070
|
-
Provision caches the approved developer token
|
|
9071
|
-
|
|
9072
|
-
|
|
9112
|
+
Provision caches the approved developer token under .odla/ with mode 0600.
|
|
9113
|
+
Local-only development provisioning may also cache that developer's service
|
|
9114
|
+
credentials there. \`provision --push-secrets\` instead stages a fresh,
|
|
9115
|
+
independently revocable credential set through the approved handshake,
|
|
9116
|
+
transfers the complete set to the exact Worker with \`wrangler secret bulk\`
|
|
9117
|
+
over stdin, and never writes its plaintext under .odla. Secret push preflights
|
|
9118
|
+
Wrangler before shown-once issuance; it never rotates sibling credentials.
|
|
9073
9119
|
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
9074
9120
|
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
9075
9121
|
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
@@ -9083,12 +9129,11 @@ Safety:
|
|
|
9083
9129
|
attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
|
|
9084
9130
|
shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
|
|
9085
9131
|
agents with browser control must open that exact URL immediately; otherwise
|
|
9086
|
-
they must give it to the human verbatim. A
|
|
9087
|
-
|
|
9088
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
9132
|
+
they must give it to the human verbatim. A device code remains only in the
|
|
9133
|
+
running process. Outside an interactive terminal the wait is capped (90s by
|
|
9089
9134
|
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
9090
|
-
with code 75
|
|
9091
|
-
|
|
9135
|
+
with code 75. Rerunning always requests and opens a new code; older clients'
|
|
9136
|
+
persisted pending state is discarded.
|
|
9092
9137
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
9093
9138
|
The email is a non-secret identity hint: never provide a password or session
|
|
9094
9139
|
token. It is the email shown by the signed-in odla account \u2014 never infer it
|
|
@@ -10993,7 +11038,7 @@ async function issueO11yToken(opts) {
|
|
|
10993
11038
|
);
|
|
10994
11039
|
if (res.status === 409 && !opts.rotateO11y) {
|
|
10995
11040
|
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 --
|
|
11041
|
+
`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
11042
|
);
|
|
10998
11043
|
}
|
|
10999
11044
|
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
|
|
@@ -11009,6 +11054,88 @@ async function safeText7(res) {
|
|
|
11009
11054
|
}
|
|
11010
11055
|
}
|
|
11011
11056
|
|
|
11057
|
+
// src/runtime-credentials.ts
|
|
11058
|
+
var import_node_crypto5 = require("crypto");
|
|
11059
|
+
function runtimeUrl(cfg, suffix = "") {
|
|
11060
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
|
|
11061
|
+
}
|
|
11062
|
+
async function safeError(response2) {
|
|
11063
|
+
const text2 = await response2.text();
|
|
11064
|
+
return redactSecrets(text2.slice(0, 1e3));
|
|
11065
|
+
}
|
|
11066
|
+
async function finish(doFetch, cfg, token, sessionId, method) {
|
|
11067
|
+
return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
|
|
11068
|
+
method,
|
|
11069
|
+
headers: { authorization: `Bearer ${token}` }
|
|
11070
|
+
});
|
|
11071
|
+
}
|
|
11072
|
+
async function deliverRuntimeCredentials(cfg, options) {
|
|
11073
|
+
const doFetch = options.fetch ?? fetch;
|
|
11074
|
+
const run = options.runner ?? defaultRunner;
|
|
11075
|
+
const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
|
|
11076
|
+
const target = await wranglerRuntimeTarget(run, {
|
|
11077
|
+
cwd: cfg.rootDir,
|
|
11078
|
+
env: wranglerEnv
|
|
11079
|
+
});
|
|
11080
|
+
const issue = await doFetch(runtimeUrl(cfg), {
|
|
11081
|
+
method: "POST",
|
|
11082
|
+
headers: {
|
|
11083
|
+
authorization: `Bearer ${options.developerToken}`,
|
|
11084
|
+
"content-type": "application/json"
|
|
11085
|
+
},
|
|
11086
|
+
body: JSON.stringify({
|
|
11087
|
+
env: options.env,
|
|
11088
|
+
idempotencyKey: `wrangler:${(0, import_node_crypto5.randomUUID)()}`,
|
|
11089
|
+
target
|
|
11090
|
+
})
|
|
11091
|
+
});
|
|
11092
|
+
const body = await issue.json().catch(() => null);
|
|
11093
|
+
if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
|
|
11094
|
+
const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
|
|
11095
|
+
throw new Error(`runtime credential staging failed: ${String(detail)}`);
|
|
11096
|
+
}
|
|
11097
|
+
const secrets = body.secrets;
|
|
11098
|
+
const values = {};
|
|
11099
|
+
for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
|
|
11100
|
+
const value2 = secrets[name];
|
|
11101
|
+
if (typeof value2 === "string" && value2) values[name] = value2;
|
|
11102
|
+
}
|
|
11103
|
+
if (Object.keys(values).length === 0) {
|
|
11104
|
+
await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
|
|
11105
|
+
throw new Error("runtime credential staging returned no configured service credentials");
|
|
11106
|
+
}
|
|
11107
|
+
const pushed = await wranglerBulkSecrets(run, {
|
|
11108
|
+
secrets: values,
|
|
11109
|
+
env: wranglerEnv,
|
|
11110
|
+
cwd: cfg.rootDir
|
|
11111
|
+
});
|
|
11112
|
+
if (pushed.code !== 0) {
|
|
11113
|
+
await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
|
|
11114
|
+
const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
|
|
11115
|
+
throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
|
|
11116
|
+
}
|
|
11117
|
+
const committed = await finish(
|
|
11118
|
+
doFetch,
|
|
11119
|
+
cfg,
|
|
11120
|
+
options.developerToken,
|
|
11121
|
+
body.sessionId,
|
|
11122
|
+
"POST"
|
|
11123
|
+
);
|
|
11124
|
+
if (!committed.ok) {
|
|
11125
|
+
await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
|
|
11126
|
+
throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
|
|
11127
|
+
}
|
|
11128
|
+
options.stdout?.log(
|
|
11129
|
+
`${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
|
|
11130
|
+
);
|
|
11131
|
+
return {
|
|
11132
|
+
sessionId: body.sessionId,
|
|
11133
|
+
target,
|
|
11134
|
+
...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
|
|
11135
|
+
...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
|
|
11136
|
+
};
|
|
11137
|
+
}
|
|
11138
|
+
|
|
11012
11139
|
// src/provision-live.ts
|
|
11013
11140
|
function liveProvisionConfig(cfg) {
|
|
11014
11141
|
if (!cfg.envs.includes("dev")) {
|
|
@@ -11042,8 +11169,10 @@ async function provision(options) {
|
|
|
11042
11169
|
if (options.rotateO11yToken && !hasO11y) {
|
|
11043
11170
|
throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
|
|
11044
11171
|
}
|
|
11045
|
-
if (options.pushSecrets && options.
|
|
11046
|
-
throw new Error(
|
|
11172
|
+
if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
|
|
11173
|
+
throw new Error(
|
|
11174
|
+
"--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
|
|
11175
|
+
);
|
|
11047
11176
|
}
|
|
11048
11177
|
out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
|
|
11049
11178
|
out.log(` platform: ${plan.platformUrl}`);
|
|
@@ -11080,7 +11209,7 @@ async function provision(options) {
|
|
|
11080
11209
|
out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
|
|
11081
11210
|
return;
|
|
11082
11211
|
}
|
|
11083
|
-
let credentials = readCredentials(cfg.local.credentialsFile);
|
|
11212
|
+
let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
|
|
11084
11213
|
if (credentials && credentials.appId !== cfg.app.id) {
|
|
11085
11214
|
throw new Error(
|
|
11086
11215
|
`credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
|
|
@@ -11089,7 +11218,7 @@ async function provision(options) {
|
|
|
11089
11218
|
const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
|
|
11090
11219
|
const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
|
|
11091
11220
|
const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
|
|
11092
|
-
const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
|
|
11221
|
+
const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
|
|
11093
11222
|
if (options.writeCredentials === false && losesShownOnceCredential) {
|
|
11094
11223
|
throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
|
|
11095
11224
|
}
|
|
@@ -11163,38 +11292,42 @@ async function provision(options) {
|
|
|
11163
11292
|
await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
|
|
11164
11293
|
}
|
|
11165
11294
|
}
|
|
11295
|
+
let devVarsCredentials = credentials;
|
|
11166
11296
|
for (const env of cfg.envs) {
|
|
11167
11297
|
const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
|
|
11168
|
-
|
|
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;
|
|
11298
|
+
let dbKey;
|
|
11180
11299
|
if (options.pushSecrets) {
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
}
|
|
11300
|
+
const delivered = await deliverRuntimeCredentials(cfg, {
|
|
11301
|
+
env,
|
|
11302
|
+
developerToken: token,
|
|
11303
|
+
fetch: doFetch,
|
|
11304
|
+
runner: options.secretRunner,
|
|
11305
|
+
stdout: out
|
|
11306
|
+
});
|
|
11307
|
+
dbKey = delivered.dbKey;
|
|
11308
|
+
devVarsCredentials = mergeCredential(devVarsCredentials, {
|
|
11309
|
+
appId: cfg.app.id,
|
|
11310
|
+
platformUrl: cfg.platformUrl,
|
|
11311
|
+
dbEndpoint: cfg.dbEndpoint,
|
|
11312
|
+
env,
|
|
11313
|
+
tenantId,
|
|
11314
|
+
...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
|
|
11315
|
+
...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
|
|
11316
|
+
});
|
|
11317
|
+
} else {
|
|
11318
|
+
credentials = await provisionEnvCredentials({
|
|
11319
|
+
cfg,
|
|
11320
|
+
env,
|
|
11321
|
+
developerToken: token,
|
|
11322
|
+
credentials,
|
|
11323
|
+
rotateDb: !!options.rotateKeys,
|
|
11324
|
+
rotateO11y: rotatesO11y,
|
|
11325
|
+
write: options.writeCredentials !== false,
|
|
11326
|
+
fetch: doFetch,
|
|
11327
|
+
stdout: out
|
|
11328
|
+
});
|
|
11329
|
+
devVarsCredentials = credentials;
|
|
11330
|
+
dbKey = credentials.envs[env]?.dbKey;
|
|
11198
11331
|
}
|
|
11199
11332
|
if (schema && dbKey) {
|
|
11200
11333
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
@@ -11218,13 +11351,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
11218
11351
|
}
|
|
11219
11352
|
}
|
|
11220
11353
|
}
|
|
11221
|
-
if (options.writeCredentials !== false && credentials) {
|
|
11354
|
+
if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
|
|
11222
11355
|
const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
|
|
11223
11356
|
out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
|
|
11224
11357
|
}
|
|
11225
|
-
if (devVarsTarget &&
|
|
11358
|
+
if (devVarsTarget && devVarsCredentials) {
|
|
11226
11359
|
const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
11227
|
-
writeDevVars(devVarsTarget,
|
|
11360
|
+
writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
|
|
11228
11361
|
out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
|
|
11229
11362
|
}
|
|
11230
11363
|
if (cfg.services.includes("calendar")) {
|
|
@@ -11315,6 +11448,7 @@ var COMMAND_SURFACE = {
|
|
|
11315
11448
|
code: { connect: {} },
|
|
11316
11449
|
config: { diff: {}, plan: {}, apply: {} },
|
|
11317
11450
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
11451
|
+
credentials: { list: {}, revoke: {} },
|
|
11318
11452
|
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
11319
11453
|
discuss: {
|
|
11320
11454
|
groups: {},
|
|
@@ -13052,6 +13186,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
13052
13186
|
await contextCommand(parsed, runtime);
|
|
13053
13187
|
return;
|
|
13054
13188
|
}
|
|
13189
|
+
if (command === "credentials") {
|
|
13190
|
+
await credentialCommand(parsed, runtime);
|
|
13191
|
+
return;
|
|
13192
|
+
}
|
|
13055
13193
|
if (command === "runbook") {
|
|
13056
13194
|
await runbookCommand(parsed, runtime);
|
|
13057
13195
|
return;
|