@odla-ai/cli 0.27.4 → 0.27.5
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 +9 -9
- package/dist/bin.cjs +95 -58
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-GENTB3VO.js → chunk-DQOJ4S6H.js} +96 -59
- package/dist/chunk-DQOJ4S6H.js.map +1 -0
- package/dist/index.cjs +95 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/references/sdks.md +5 -4
- package/dist/chunk-GENTB3VO.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
Project-neutral provisioning CLI for odla apps. It creates and validates an
|
|
9
9
|
`odla.config.mjs`, then uses that config to register an app, enable services,
|
|
10
|
-
push odla-db schema/rules, configure
|
|
10
|
+
push odla-db schema/rules, configure hosted app AI (or explicit BYOK), configure Clerk auth, record
|
|
11
11
|
deployment links, compose declared npm capability integrations, connect Google Calendar booking, provision o11y ingest credentials, and transfer local or
|
|
12
12
|
deployed Worker secrets without printing them. It also gives platform admins a
|
|
13
13
|
scoped System AI control surface and gives app owners a provider-key-free
|
|
14
14
|
hosted security command.
|
|
15
15
|
|
|
16
16
|
Provisioning does not know about any specific app. App identity, environments,
|
|
17
|
-
services, schema, rules, integrations, auth, AI
|
|
17
|
+
services, schema, rules, integrations, auth, AI mode/model, and links all come
|
|
18
18
|
from config. Operator commands can also receive explicit platform/app/env
|
|
19
19
|
context so remote agents are not forced to manufacture a project checkout.
|
|
20
20
|
|
|
@@ -485,8 +485,8 @@ shown-once credential.
|
|
|
485
485
|
5. Collision-checks and merges declared integration schema/rule fragments,
|
|
486
486
|
pushes the composed database contract, and creates integration seeds only
|
|
487
487
|
when their natural-key rows are absent.
|
|
488
|
-
6. Configures
|
|
489
|
-
configured key env var is set.
|
|
488
|
+
6. Configures hosted app AI by default. An explicit BYOK config stores its
|
|
489
|
+
provider key in the tenant vault when the configured key env var is set.
|
|
490
490
|
7. For calendar, reads owner-visible connection status and, when needed, asks
|
|
491
491
|
the platform for a state-bound Google authorization URL and opens only the
|
|
492
492
|
exact Google OAuth endpoint (or a same-platform interstitial). The human completes consent there;
|
|
@@ -730,7 +730,8 @@ change events with actor attribution; credential values never enter that trail.
|
|
|
730
730
|
It never returns prompts, repository source, model output, reports, or provider
|
|
731
731
|
credentials. Credential writes accept only
|
|
732
732
|
`--from-env <NAME>` or `--stdin` and never place the value in argv, output, or
|
|
733
|
-
the cache. System AI and
|
|
733
|
+
the cache. System AI and hosted app AI have separate routes and accounting;
|
|
734
|
+
explicit BYOK has its own app-tenant vault path.
|
|
734
735
|
Discovery and validation provider/model settings are authoritative server
|
|
735
736
|
policy: a security command has no field that can override either route. Admin
|
|
736
737
|
bounds can only narrow the hard ceilings of 64 calls per role/run and, for each
|
|
@@ -829,10 +830,9 @@ export default {
|
|
|
829
830
|
defaultRules: "deny",
|
|
830
831
|
publicRead: [], // namespaces where a `view: "true"` rule is intentional
|
|
831
832
|
},
|
|
832
|
-
ai: {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
},
|
|
833
|
+
ai: { mode: "hosted" },
|
|
834
|
+
// BYOK compatibility instead:
|
|
835
|
+
// ai: { mode: "byok", provider: "anthropic", keyEnv: "ANTHROPIC_API_KEY" },
|
|
836
836
|
calendar: {
|
|
837
837
|
google: {
|
|
838
838
|
availabilityCalendars: { dev: ["primary"] },
|
package/dist/bin.cjs
CHANGED
|
@@ -1001,6 +1001,32 @@ var import_node_path4 = require("path");
|
|
|
1001
1001
|
var import_node_url = require("url");
|
|
1002
1002
|
var import_apps = require("@odla-ai/apps");
|
|
1003
1003
|
|
|
1004
|
+
// src/ai-config-validation.ts
|
|
1005
|
+
function validateAiConfig(cfg, path) {
|
|
1006
|
+
if (cfg.ai === void 0) return;
|
|
1007
|
+
if (!isRecord4(cfg.ai)) throw new Error(`${path}: ai must be an object`);
|
|
1008
|
+
assertOnly(cfg.ai, ["mode", "provider", "model", "keyEnv", "secretName"], `${path}: ai`);
|
|
1009
|
+
if (cfg.ai.mode !== void 0 && cfg.ai.mode !== "hosted" && cfg.ai.mode !== "byok") {
|
|
1010
|
+
throw new Error(`${path}: ai.mode must be hosted or byok`);
|
|
1011
|
+
}
|
|
1012
|
+
if (cfg.ai.mode === "byok" && !safeText(cfg.ai.provider, 100)) {
|
|
1013
|
+
throw new Error(`${path}: ai.provider is required when ai.mode is byok`);
|
|
1014
|
+
}
|
|
1015
|
+
if (cfg.ai.mode === "hosted" && (cfg.ai.provider || cfg.ai.keyEnv || cfg.ai.secretName)) {
|
|
1016
|
+
throw new Error(`${path}: hosted ai cannot configure a provider, keyEnv, or secretName`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function assertOnly(value2, allowed, label) {
|
|
1020
|
+
const extra = Object.keys(value2).find((key) => !allowed.includes(key));
|
|
1021
|
+
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
1022
|
+
}
|
|
1023
|
+
function isRecord4(value2) {
|
|
1024
|
+
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
1025
|
+
}
|
|
1026
|
+
function safeText(value2, max) {
|
|
1027
|
+
return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1004
1030
|
// src/integration-validation.ts
|
|
1005
1031
|
function validateIntegrations(cfg, path, defaultServices) {
|
|
1006
1032
|
if (cfg.integrations === void 0) return;
|
|
@@ -1008,16 +1034,16 @@ function validateIntegrations(cfg, path, defaultServices) {
|
|
|
1008
1034
|
const ids = /* @__PURE__ */ new Set();
|
|
1009
1035
|
for (const [index, integration] of cfg.integrations.entries()) {
|
|
1010
1036
|
const at = `${path}: integrations[${index}]`;
|
|
1011
|
-
if (!
|
|
1037
|
+
if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
|
|
1012
1038
|
if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
|
|
1013
1039
|
if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
|
|
1014
1040
|
ids.add(integration.id);
|
|
1015
|
-
if (!
|
|
1016
|
-
if (!
|
|
1017
|
-
if (integration.schema !== void 0 && (!
|
|
1041
|
+
if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
|
|
1042
|
+
if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
|
|
1043
|
+
if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
|
|
1018
1044
|
throw new Error(`${at}.schema must contain an entities object`);
|
|
1019
1045
|
}
|
|
1020
|
-
if (integration.rules !== void 0 && !
|
|
1046
|
+
if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
|
|
1021
1047
|
validateSeeds(integration, at);
|
|
1022
1048
|
validateProbes(integration, at);
|
|
1023
1049
|
}
|
|
@@ -1031,13 +1057,13 @@ function validateSeeds(integration, at) {
|
|
|
1031
1057
|
const ids = /* @__PURE__ */ new Set();
|
|
1032
1058
|
for (const [index, seed] of integration.seeds.entries()) {
|
|
1033
1059
|
const sat = `${at}.seeds[${index}]`;
|
|
1034
|
-
if (!
|
|
1060
|
+
if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
|
|
1035
1061
|
if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
|
|
1036
1062
|
ids.add(seed.id);
|
|
1037
|
-
if (!
|
|
1063
|
+
if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
|
|
1038
1064
|
throw new Error(`${sat}.key requires string attr and value`);
|
|
1039
1065
|
}
|
|
1040
|
-
if (!
|
|
1066
|
+
if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
|
|
1041
1067
|
if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
|
|
1042
1068
|
throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
|
|
1043
1069
|
}
|
|
@@ -1048,16 +1074,16 @@ function validateProbes(integration, at) {
|
|
|
1048
1074
|
if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
|
|
1049
1075
|
for (const [index, probe] of integration.probes.entries()) {
|
|
1050
1076
|
const pat = `${at}.probes[${index}]`;
|
|
1051
|
-
if (!
|
|
1077
|
+
if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
|
|
1052
1078
|
if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
|
|
1053
1079
|
throw new Error(`${pat}.expectedStatus must be an HTTP status`);
|
|
1054
1080
|
}
|
|
1055
1081
|
}
|
|
1056
1082
|
}
|
|
1057
|
-
function
|
|
1083
|
+
function isRecord5(value2) {
|
|
1058
1084
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
1059
1085
|
}
|
|
1060
|
-
function
|
|
1086
|
+
function safeText2(value2, max) {
|
|
1061
1087
|
return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
|
|
1062
1088
|
}
|
|
1063
1089
|
function safeProbePath(value2) {
|
|
@@ -1187,6 +1213,7 @@ function validateRawConfig(raw, path) {
|
|
|
1187
1213
|
if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
|
|
1188
1214
|
throw new Error(`${path}: services must be an array of non-empty names`);
|
|
1189
1215
|
}
|
|
1216
|
+
validateAiConfig(cfg, path);
|
|
1190
1217
|
validateIntegrations(cfg, path, DEFAULT_SERVICES);
|
|
1191
1218
|
}
|
|
1192
1219
|
function validateCalendarConfig(cfg, envs, services, path) {
|
|
@@ -1195,11 +1222,11 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1195
1222
|
if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
|
|
1196
1223
|
return;
|
|
1197
1224
|
}
|
|
1198
|
-
if (!
|
|
1199
|
-
|
|
1200
|
-
if (!
|
|
1225
|
+
if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
|
|
1226
|
+
assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
|
|
1227
|
+
if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
1201
1228
|
const google = cfg.calendar.google;
|
|
1202
|
-
|
|
1229
|
+
assertOnly2(
|
|
1203
1230
|
google,
|
|
1204
1231
|
["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
|
|
1205
1232
|
`${path}: calendar.google`
|
|
@@ -1209,7 +1236,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1209
1236
|
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
1210
1237
|
}
|
|
1211
1238
|
const availability = google[availabilityKey];
|
|
1212
|
-
if (!
|
|
1239
|
+
if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
1213
1240
|
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
1214
1241
|
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
1215
1242
|
for (const env of envs) {
|
|
@@ -1220,22 +1247,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1220
1247
|
if (ids.length > 10) {
|
|
1221
1248
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
1222
1249
|
}
|
|
1223
|
-
if (ids.some((id) => !
|
|
1250
|
+
if (ids.some((id) => !safeText3(id, 1024))) {
|
|
1224
1251
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
1225
1252
|
}
|
|
1226
1253
|
}
|
|
1227
1254
|
if (google.bookingCalendar !== void 0) {
|
|
1228
|
-
if (!
|
|
1255
|
+
if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
1229
1256
|
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
1230
1257
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
1231
1258
|
for (const [env, value2] of Object.entries(google.bookingCalendar)) {
|
|
1232
|
-
if (!
|
|
1259
|
+
if (!safeText3(value2, 1024)) {
|
|
1233
1260
|
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
1234
1261
|
}
|
|
1235
1262
|
}
|
|
1236
1263
|
}
|
|
1237
1264
|
if (google.bookingPageUrl !== void 0) {
|
|
1238
|
-
if (!
|
|
1265
|
+
if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
|
|
1239
1266
|
const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
|
|
1240
1267
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
|
|
1241
1268
|
for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
|
|
@@ -1258,14 +1285,14 @@ function validateServices(services, path) {
|
|
|
1258
1285
|
}
|
|
1259
1286
|
}
|
|
1260
1287
|
}
|
|
1261
|
-
function
|
|
1288
|
+
function assertOnly2(value2, allowed, label) {
|
|
1262
1289
|
const extra = Object.keys(value2).find((key) => !allowed.includes(key));
|
|
1263
1290
|
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
1264
1291
|
}
|
|
1265
|
-
function
|
|
1292
|
+
function isRecord6(value2) {
|
|
1266
1293
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
1267
1294
|
}
|
|
1268
|
-
function
|
|
1295
|
+
function safeText3(value2, max) {
|
|
1269
1296
|
return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
|
|
1270
1297
|
}
|
|
1271
1298
|
function safeHttpsUrl(value2) {
|
|
@@ -3062,7 +3089,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
3062
3089
|
`${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`
|
|
3063
3090
|
);
|
|
3064
3091
|
}
|
|
3065
|
-
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await
|
|
3092
|
+
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
|
|
3066
3093
|
}
|
|
3067
3094
|
async function postJson(doFetch, url, bearer, body) {
|
|
3068
3095
|
const res = await doFetch(url, {
|
|
@@ -3070,7 +3097,7 @@ async function postJson(doFetch, url, bearer, body) {
|
|
|
3070
3097
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
3071
3098
|
body: JSON.stringify(body)
|
|
3072
3099
|
});
|
|
3073
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
3100
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
|
|
3074
3101
|
}
|
|
3075
3102
|
function normalizeClerkConfig(value2) {
|
|
3076
3103
|
if (!value2) return null;
|
|
@@ -3083,7 +3110,7 @@ function normalizeClerkConfig(value2) {
|
|
|
3083
3110
|
const publishableKey = envValue(cfg.publishableKey);
|
|
3084
3111
|
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
3085
3112
|
}
|
|
3086
|
-
async function
|
|
3113
|
+
async function safeText4(res) {
|
|
3087
3114
|
try {
|
|
3088
3115
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
3089
3116
|
} catch {
|
|
@@ -4065,18 +4092,18 @@ function assertSeedContracts(integrations, schema) {
|
|
|
4065
4092
|
}
|
|
4066
4093
|
}
|
|
4067
4094
|
function isUniqueAttr(schema, ns, attr) {
|
|
4068
|
-
if (!
|
|
4095
|
+
if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
|
|
4069
4096
|
const entity = schema.entities[ns];
|
|
4070
|
-
if (!
|
|
4097
|
+
if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
|
|
4071
4098
|
const definition = entity.attrs[attr];
|
|
4072
|
-
return
|
|
4099
|
+
return isRecord7(definition) && definition.unique === true;
|
|
4073
4100
|
}
|
|
4074
4101
|
function normalizeSchema(value2) {
|
|
4075
4102
|
if (value2 === void 0 || value2 === null) return { entities: {}, links: {} };
|
|
4076
|
-
if (!
|
|
4103
|
+
if (!isRecord7(value2) || !isRecord7(value2.entities)) {
|
|
4077
4104
|
throw new Error("db schema must be a serialized schema object with an entities map");
|
|
4078
4105
|
}
|
|
4079
|
-
if (value2.links !== void 0 && !
|
|
4106
|
+
if (value2.links !== void 0 && !isRecord7(value2.links)) throw new Error("db schema links must be an object");
|
|
4080
4107
|
return {
|
|
4081
4108
|
entities: { ...value2.entities },
|
|
4082
4109
|
links: { ...value2.links ?? {} }
|
|
@@ -4090,7 +4117,7 @@ function mergeMap(target, fragment, label) {
|
|
|
4090
4117
|
target[name] = value2;
|
|
4091
4118
|
}
|
|
4092
4119
|
}
|
|
4093
|
-
function
|
|
4120
|
+
function isRecord7(value2) {
|
|
4094
4121
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
4095
4122
|
}
|
|
4096
4123
|
|
|
@@ -4109,7 +4136,7 @@ async function doctor(options) {
|
|
|
4109
4136
|
out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
4110
4137
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
4111
4138
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
4112
|
-
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider
|
|
4139
|
+
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4113
4140
|
if (cfg.services.includes("calendar")) {
|
|
4114
4141
|
const calendar = cfg.envs.map((env) => {
|
|
4115
4142
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -4130,7 +4157,9 @@ async function doctor(options) {
|
|
|
4130
4157
|
}
|
|
4131
4158
|
}
|
|
4132
4159
|
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
4133
|
-
if (cfg.services.includes("ai") &&
|
|
4160
|
+
if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
|
|
4161
|
+
warnings.push("ai.mode is byok but ai.provider is not set");
|
|
4162
|
+
}
|
|
4134
4163
|
if (cfg.auth?.clerk) {
|
|
4135
4164
|
for (const [env, value2] of Object.entries(cfg.auth.clerk)) {
|
|
4136
4165
|
if (typeof value2 === "string" && value2.startsWith("$") && !process.env[value2.slice(1)]) {
|
|
@@ -4210,7 +4239,7 @@ function initProject(options) {
|
|
|
4210
4239
|
if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
|
|
4211
4240
|
}
|
|
4212
4241
|
}
|
|
4213
|
-
const aiProvider = options.aiProvider
|
|
4242
|
+
const aiProvider = options.aiProvider;
|
|
4214
4243
|
(0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
|
|
4215
4244
|
(0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4216
4245
|
(0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
|
|
@@ -4237,6 +4266,15 @@ function configTemplate(input) {
|
|
|
4237
4266
|
},
|
|
4238
4267
|
},
|
|
4239
4268
|
` : "";
|
|
4269
|
+
const ai = input.aiProvider ? ` ai: {
|
|
4270
|
+
mode: "byok",
|
|
4271
|
+
provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
|
|
4272
|
+
// Optional: set this env var while running provision to store the provider
|
|
4273
|
+
// key in the app vault for each tenant.
|
|
4274
|
+
keyEnv: "${defaultKeyEnv(input.aiProvider)}",
|
|
4275
|
+
},` : ` // Hosted AI uses admin-approved models and central cost tracking.
|
|
4276
|
+
// Pass --ai-provider during init only when this app must use BYOK.
|
|
4277
|
+
ai: { mode: "hosted" },`;
|
|
4240
4278
|
return `export default {
|
|
4241
4279
|
platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
|
|
4242
4280
|
dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
|
|
@@ -4255,12 +4293,7 @@ function configTemplate(input) {
|
|
|
4255
4293
|
// When rules is omitted, the CLI generates deny-all rules from schema.
|
|
4256
4294
|
defaultRules: "deny",
|
|
4257
4295
|
},
|
|
4258
|
-
|
|
4259
|
-
provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
|
|
4260
|
-
// Optional: set this env var while running provision to store the provider
|
|
4261
|
-
// key in the platform vault for each tenant.
|
|
4262
|
-
keyEnv: "${defaultKeyEnv(input.aiProvider)}",
|
|
4263
|
-
},
|
|
4296
|
+
${ai}
|
|
4264
4297
|
${calendar}
|
|
4265
4298
|
auth: {
|
|
4266
4299
|
clerk: {
|
|
@@ -4756,12 +4789,16 @@ async function smoke(options) {
|
|
|
4756
4789
|
out.log(` tenant: ${entry.tenantId}`);
|
|
4757
4790
|
const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
|
|
4758
4791
|
out.log(` public-config: ok`);
|
|
4759
|
-
if (cfg.ai?.provider) {
|
|
4792
|
+
if (cfg.services.includes("ai") && cfg.ai?.provider) {
|
|
4760
4793
|
const provider = publicConfig.ai?.provider ?? null;
|
|
4761
4794
|
if (provider !== cfg.ai.provider) {
|
|
4762
4795
|
throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
|
|
4763
4796
|
}
|
|
4764
|
-
out.log(` ai:
|
|
4797
|
+
out.log(` ai: byok/${provider}`);
|
|
4798
|
+
} else if (cfg.services.includes("ai")) {
|
|
4799
|
+
const mode = publicConfig.ai?.mode;
|
|
4800
|
+
if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
|
|
4801
|
+
out.log(" ai: hosted");
|
|
4765
4802
|
}
|
|
4766
4803
|
if (hasO11y) out.log(` o11y: credentials present`);
|
|
4767
4804
|
if (cfg.services.includes("calendar")) {
|
|
@@ -4839,7 +4876,7 @@ async function getJson(doFetch, url, bearer) {
|
|
|
4839
4876
|
const res = await doFetch(url, {
|
|
4840
4877
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
4841
4878
|
});
|
|
4842
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4879
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
4843
4880
|
return res.json();
|
|
4844
4881
|
}
|
|
4845
4882
|
async function postJson2(doFetch, url, bearer, body) {
|
|
@@ -4848,7 +4885,7 @@ async function postJson2(doFetch, url, bearer, body) {
|
|
|
4848
4885
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
4849
4886
|
body: JSON.stringify(body)
|
|
4850
4887
|
});
|
|
4851
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4888
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
4852
4889
|
return res.json();
|
|
4853
4890
|
}
|
|
4854
4891
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -4856,7 +4893,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
4856
4893
|
url.searchParams.set("env", env);
|
|
4857
4894
|
return url.toString();
|
|
4858
4895
|
}
|
|
4859
|
-
async function
|
|
4896
|
+
async function safeText5(res) {
|
|
4860
4897
|
try {
|
|
4861
4898
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
4862
4899
|
} catch {
|
|
@@ -8686,7 +8723,7 @@ Start here:
|
|
|
8686
8723
|
|
|
8687
8724
|
Usage:
|
|
8688
8725
|
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8689
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
8726
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
|
|
8690
8727
|
odla-ai doctor [--config odla.config.mjs]
|
|
8691
8728
|
odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8692
8729
|
odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
|
|
@@ -10679,7 +10716,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
10679
10716
|
const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
|
|
10680
10717
|
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
10681
10718
|
});
|
|
10682
|
-
const rows =
|
|
10719
|
+
const rows = isRecord8(payload) && isRecord8(payload.result) ? payload.result[seed.ns] : void 0;
|
|
10683
10720
|
if (!Array.isArray(rows)) {
|
|
10684
10721
|
throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
|
|
10685
10722
|
}
|
|
@@ -10711,7 +10748,7 @@ async function postJson3(doFetch, url, bearer, body) {
|
|
|
10711
10748
|
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
|
|
10712
10749
|
return res.json().catch(() => ({}));
|
|
10713
10750
|
}
|
|
10714
|
-
function
|
|
10751
|
+
function isRecord8(value2) {
|
|
10715
10752
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
10716
10753
|
}
|
|
10717
10754
|
async function responseText(res) {
|
|
@@ -10781,14 +10818,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
10781
10818
|
appId: tenantId
|
|
10782
10819
|
})
|
|
10783
10820
|
});
|
|
10784
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
10821
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
|
|
10785
10822
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
10786
10823
|
method: "POST",
|
|
10787
10824
|
headers,
|
|
10788
10825
|
body: "{}"
|
|
10789
10826
|
});
|
|
10790
10827
|
}
|
|
10791
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
10828
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
|
|
10792
10829
|
const body = await res.json();
|
|
10793
10830
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
10794
10831
|
return body.key;
|
|
@@ -10804,12 +10841,12 @@ async function issueO11yToken(opts) {
|
|
|
10804
10841
|
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
10805
10842
|
);
|
|
10806
10843
|
}
|
|
10807
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
10844
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
|
|
10808
10845
|
const body = await res.json();
|
|
10809
10846
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
10810
10847
|
return body.token;
|
|
10811
10848
|
}
|
|
10812
|
-
async function
|
|
10849
|
+
async function safeText6(res) {
|
|
10813
10850
|
try {
|
|
10814
10851
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
10815
10852
|
} catch {
|
|
@@ -10852,7 +10889,7 @@ async function provision(options) {
|
|
|
10852
10889
|
const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
|
|
10853
10890
|
out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
|
|
10854
10891
|
}
|
|
10855
|
-
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "
|
|
10892
|
+
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "hosted" : "not enabled"}`);
|
|
10856
10893
|
if (cfg.services.includes("calendar")) {
|
|
10857
10894
|
for (const env of cfg.envs) {
|
|
10858
10895
|
const calendar = calendarServiceConfig(cfg, env);
|
|
@@ -10903,11 +10940,11 @@ async function provision(options) {
|
|
|
10903
10940
|
for (const service of serviceOrder) {
|
|
10904
10941
|
if (service === "ai") {
|
|
10905
10942
|
if (cfg.ai?.provider) {
|
|
10906
|
-
await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
|
|
10907
|
-
out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
|
|
10943
|
+
await apps.setAi(cfg.app.id, env, { mode: "byok", provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
|
|
10944
|
+
out.log(`${env}: ai configured (byok ${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
|
|
10908
10945
|
} else {
|
|
10909
|
-
await apps.
|
|
10910
|
-
out.log(`${env}: ai
|
|
10946
|
+
await apps.setAi(cfg.app.id, env, { mode: "hosted", ...cfg.ai?.model ? { model: cfg.ai.model } : {} });
|
|
10947
|
+
out.log(`${env}: ai configured (hosted${cfg.ai?.model ? `/${cfg.ai.model}` : ""})`);
|
|
10911
10948
|
}
|
|
10912
10949
|
} else {
|
|
10913
10950
|
const config = service === "calendar" ? calendarServiceConfig(cfg, env) : void 0;
|