@odla-ai/cli 0.8.0 → 0.9.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/README.md +20 -2
- package/dist/bin.cjs +116 -32
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-BHAEDEL2.js → chunk-MJYQ7YDH.js} +112 -26
- package/dist/chunk-MJYQ7YDH.js.map +1 -0
- package/dist/index.cjs +120 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -219
- package/dist/index.d.ts +54 -219
- package/dist/index.js +5 -1
- package/llms.txt +75 -222
- package/package.json +1 -1
- package/skills/odla-migrate/references/phase-3b-user-sync.md +15 -8
- package/skills/odla-migrate/references/secrets-map.md +16 -2
- package/dist/chunk-BHAEDEL2.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,8 @@ __export(index_exports, {
|
|
|
57
57
|
runCli: () => runCli,
|
|
58
58
|
runHostedSecurity: () => runHostedSecurity,
|
|
59
59
|
secretsPush: () => secretsPush,
|
|
60
|
+
secretsSet: () => secretsSet,
|
|
61
|
+
secretsSetClerkKey: () => secretsSetClerkKey,
|
|
60
62
|
smoke: () => smoke,
|
|
61
63
|
startHostedSecurityJob: () => startHostedSecurityJob
|
|
62
64
|
});
|
|
@@ -67,7 +69,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
67
69
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
68
70
|
|
|
69
71
|
// src/admin-ai.ts
|
|
70
|
-
var
|
|
72
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
71
73
|
|
|
72
74
|
// src/token.ts
|
|
73
75
|
var import_db = require("@odla-ai/db");
|
|
@@ -297,9 +299,32 @@ function platformAudience(value) {
|
|
|
297
299
|
return url.origin;
|
|
298
300
|
}
|
|
299
301
|
|
|
302
|
+
// src/secret-input.ts
|
|
303
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
304
|
+
var MAX_BYTES = 64 * 1024;
|
|
305
|
+
async function secretInputValue(options, kind = "credential") {
|
|
306
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
307
|
+
let value;
|
|
308
|
+
if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
|
|
309
|
+
else if (options.stdin) value = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
310
|
+
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
311
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
312
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : `stdin ${kind} is empty`);
|
|
313
|
+
if (new TextEncoder().encode(value).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
async function readSecretStream(kind, stream = import_node_process3.default.stdin) {
|
|
317
|
+
let value = "";
|
|
318
|
+
for await (const chunk of stream) {
|
|
319
|
+
value += String(chunk);
|
|
320
|
+
if (value.length > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
321
|
+
}
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
|
|
300
325
|
// src/admin-ai-auth.ts
|
|
301
326
|
var import_node_fs2 = require("fs");
|
|
302
|
-
var
|
|
327
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
303
328
|
var import_db2 = require("@odla-ai/db");
|
|
304
329
|
async function getScopedPlatformToken(options) {
|
|
305
330
|
return resolveAdminPlatformToken(options);
|
|
@@ -307,7 +332,7 @@ async function getScopedPlatformToken(options) {
|
|
|
307
332
|
async function resolveAdminPlatformToken(options) {
|
|
308
333
|
const audience = platformAudience(options.platform);
|
|
309
334
|
if (options.token) return options.token;
|
|
310
|
-
const fromEnv =
|
|
335
|
+
const fromEnv = import_node_process4.default.env.ODLA_ADMIN_TOKEN;
|
|
311
336
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
312
337
|
return scopedToken(
|
|
313
338
|
audience,
|
|
@@ -319,7 +344,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
319
344
|
}
|
|
320
345
|
function audienceBoundEnvToken(token, platform) {
|
|
321
346
|
const audience = platformAudience(platform);
|
|
322
|
-
const declared =
|
|
347
|
+
const declared = import_node_process4.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
323
348
|
if (declared) {
|
|
324
349
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
325
350
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -329,7 +354,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
329
354
|
}
|
|
330
355
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
331
356
|
const audience = platformAudience(platform);
|
|
332
|
-
const tokenFile = options.tokenFile ?? `${
|
|
357
|
+
const tokenFile = options.tokenFile ?? `${import_node_process4.default.cwd()}/.odla/admin-token.local.json`;
|
|
333
358
|
const cache = readJsonFile(tokenFile);
|
|
334
359
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
335
360
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
@@ -344,13 +369,13 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
344
369
|
onCode: async ({ userCode, expiresIn }) => {
|
|
345
370
|
const approvalUrl = handshakeUrl(audience, userCode);
|
|
346
371
|
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
347
|
-
const shouldOpen = options.open === true || options.open !== false && !
|
|
372
|
+
const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
|
|
348
373
|
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
349
374
|
}
|
|
350
375
|
});
|
|
351
376
|
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
352
377
|
tokens[scope] = { token, expiresAt };
|
|
353
|
-
if ((0, import_node_fs2.existsSync)(`${
|
|
378
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process4.default.cwd()}/.git`)) ensureGitignore(import_node_process4.default.cwd(), [tokenFile]);
|
|
354
379
|
writePrivateJson(tokenFile, { platform: audience, tokens });
|
|
355
380
|
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
356
381
|
return token;
|
|
@@ -511,7 +536,7 @@ function isRecord2(value) {
|
|
|
511
536
|
|
|
512
537
|
// src/admin-ai.ts
|
|
513
538
|
async function adminAi(options) {
|
|
514
|
-
const platform = platformAudience(options.platform ??
|
|
539
|
+
const platform = platformAudience(options.platform ?? import_node_process5.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
515
540
|
const doFetch = options.fetch ?? fetch;
|
|
516
541
|
const out = options.stdout ?? console;
|
|
517
542
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -550,7 +575,7 @@ async function adminAi(options) {
|
|
|
550
575
|
}
|
|
551
576
|
if (options.action === "credential-set") {
|
|
552
577
|
const provider = requireProvider(options.credentialProvider);
|
|
553
|
-
const value = await
|
|
578
|
+
const value = await secretInputValue(options);
|
|
554
579
|
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
555
580
|
method: "PUT",
|
|
556
581
|
headers,
|
|
@@ -660,25 +685,6 @@ function requireProvider(value) {
|
|
|
660
685
|
}
|
|
661
686
|
return value;
|
|
662
687
|
}
|
|
663
|
-
async function credentialValue(options) {
|
|
664
|
-
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
665
|
-
let value;
|
|
666
|
-
if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
|
|
667
|
-
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
668
|
-
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
669
|
-
value = value?.replace(/[\r\n]+$/, "");
|
|
670
|
-
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
671
|
-
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
672
|
-
return value;
|
|
673
|
-
}
|
|
674
|
-
async function readStdin() {
|
|
675
|
-
let value = "";
|
|
676
|
-
for await (const chunk of import_node_process4.default.stdin) {
|
|
677
|
-
value += String(chunk);
|
|
678
|
-
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
679
|
-
}
|
|
680
|
-
return value;
|
|
681
|
-
}
|
|
682
688
|
function policyArray(body) {
|
|
683
689
|
if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
|
|
684
690
|
if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
|
|
@@ -848,6 +854,7 @@ var CAPABILITIES = {
|
|
|
848
854
|
"register the app and enable configured services per environment",
|
|
849
855
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
850
856
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
857
|
+
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
851
858
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
852
859
|
"validate config offline and smoke-test a provisioned db environment",
|
|
853
860
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
@@ -1304,6 +1311,8 @@ Usage:
|
|
|
1304
1311
|
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1305
1312
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1306
1313
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1314
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
|
|
1315
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
|
|
1307
1316
|
odla-ai version
|
|
1308
1317
|
|
|
1309
1318
|
Commands:
|
|
@@ -1317,7 +1326,9 @@ Commands:
|
|
|
1317
1326
|
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1318
1327
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1319
1328
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1320
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
1329
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
1330
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
1331
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
1321
1332
|
version Print the CLI version.
|
|
1322
1333
|
|
|
1323
1334
|
Safety:
|
|
@@ -1475,7 +1486,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
1475
1486
|
// src/provision.ts
|
|
1476
1487
|
var import_apps2 = require("@odla-ai/apps");
|
|
1477
1488
|
var import_ai = require("@odla-ai/ai");
|
|
1478
|
-
var
|
|
1489
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
1479
1490
|
|
|
1480
1491
|
// src/provision-credentials.ts
|
|
1481
1492
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -1779,7 +1790,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1779
1790
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1780
1791
|
}
|
|
1781
1792
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1782
|
-
const key =
|
|
1793
|
+
const key = import_node_process6.default.env[cfg.ai.keyEnv];
|
|
1783
1794
|
if (key) {
|
|
1784
1795
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1785
1796
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1839,6 +1850,63 @@ async function safeText2(res) {
|
|
|
1839
1850
|
}
|
|
1840
1851
|
}
|
|
1841
1852
|
|
|
1853
|
+
// src/secrets-set.ts
|
|
1854
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
1855
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
1856
|
+
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
1857
|
+
async function secretsSet(options) {
|
|
1858
|
+
const name = (options.name ?? "").trim();
|
|
1859
|
+
if (!name) throw new Error('secret name is required, e.g. "odla-ai secrets set clerk_webhook_secret --env dev --stdin"');
|
|
1860
|
+
if (name.startsWith("$")) {
|
|
1861
|
+
throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
|
|
1862
|
+
}
|
|
1863
|
+
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
1864
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
1865
|
+
try {
|
|
1866
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
|
|
1869
|
+
}
|
|
1870
|
+
out.log(`${name} stored in the ${tenantId} vault (write-only; the value was never echoed and cannot be read back here)`);
|
|
1871
|
+
}
|
|
1872
|
+
async function secretsSetClerkKey(options) {
|
|
1873
|
+
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
1874
|
+
if (!value.startsWith("sk_")) throw new Error("the Clerk secret key must start with sk_ (sk_test_\u2026 or sk_live_\u2026)");
|
|
1875
|
+
if (value.startsWith("sk_test_") && PROD_ENV_NAMES2.has(options.env)) {
|
|
1876
|
+
throw new Error(`refusing to store an sk_test_ Clerk key for "${options.env}" \u2014 use the production instance's sk_live_ key`);
|
|
1877
|
+
}
|
|
1878
|
+
if (value.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
1879
|
+
throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
|
|
1880
|
+
}
|
|
1881
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
1882
|
+
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
1883
|
+
method: "POST",
|
|
1884
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1885
|
+
body: JSON.stringify({ value })
|
|
1886
|
+
});
|
|
1887
|
+
if (!res.ok) {
|
|
1888
|
+
const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value);
|
|
1889
|
+
throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
|
|
1890
|
+
}
|
|
1891
|
+
out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
|
|
1892
|
+
}
|
|
1893
|
+
function scrubValue(text, value) {
|
|
1894
|
+
return redactSecrets(text).split(value).join("[value redacted]");
|
|
1895
|
+
}
|
|
1896
|
+
async function resolveVaultWrite(options) {
|
|
1897
|
+
const out = options.stdout ?? console;
|
|
1898
|
+
const doFetch = options.fetch ?? fetch;
|
|
1899
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1900
|
+
if (!cfg.envs.includes(options.env)) {
|
|
1901
|
+
throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
1902
|
+
}
|
|
1903
|
+
if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
1904
|
+
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
1905
|
+
}
|
|
1906
|
+
const value = await secretInputValue(options, "secret");
|
|
1907
|
+
return { cfg, tenantId: (0, import_apps3.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1842
1910
|
// src/security-command-context.ts
|
|
1843
1911
|
var import_promises = require("readline/promises");
|
|
1844
1912
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
@@ -3180,8 +3248,26 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
3180
3248
|
}
|
|
3181
3249
|
if (command === "secrets") {
|
|
3182
3250
|
const sub = parsed.positionals[1];
|
|
3251
|
+
if (sub === "set" || sub === "set-clerk-key") {
|
|
3252
|
+
assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
|
|
3253
|
+
const options = {
|
|
3254
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
3255
|
+
name: parsed.positionals[2],
|
|
3256
|
+
env: requiredString(parsed.options.env, "--env"),
|
|
3257
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
3258
|
+
stdin: parsed.options.stdin === true,
|
|
3259
|
+
token: stringOpt(parsed.options.token),
|
|
3260
|
+
yes: parsed.options.yes === true,
|
|
3261
|
+
fetch: dependencies.fetch,
|
|
3262
|
+
stdout: dependencies.stdout
|
|
3263
|
+
};
|
|
3264
|
+
await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3183
3267
|
if (sub !== "push") {
|
|
3184
|
-
throw new Error(
|
|
3268
|
+
throw new Error(
|
|
3269
|
+
`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev", "odla-ai secrets set <name> --env dev --stdin", or "odla-ai secrets set-clerk-key --env dev --stdin".`
|
|
3270
|
+
);
|
|
3185
3271
|
}
|
|
3186
3272
|
assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
|
|
3187
3273
|
await secretsPush({
|
|
@@ -3250,6 +3336,8 @@ async function provisionCommand(parsed) {
|
|
|
3250
3336
|
runCli,
|
|
3251
3337
|
runHostedSecurity,
|
|
3252
3338
|
secretsPush,
|
|
3339
|
+
secretsSet,
|
|
3340
|
+
secretsSetClerkKey,
|
|
3253
3341
|
smoke,
|
|
3254
3342
|
startHostedSecurityJob
|
|
3255
3343
|
});
|