@odla-ai/cli 0.12.0 → 0.13.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 +43 -7
- package/REQUIREMENTS.md +11 -3
- package/dist/bin.cjs +318 -60
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-Y4HWUEFF.js → chunk-I7XDJZB6.js} +314 -56
- package/dist/chunk-I7XDJZB6.js.map +1 -0
- package/dist/index.cjs +318 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -13
- package/dist/index.d.ts +66 -13
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +5 -3
- package/skills/odla/references/build.md +9 -2
- package/skills/odla/references/sdks.md +25 -0
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/dist/chunk-Y4HWUEFF.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -954,6 +954,77 @@ var import_promises = require("stream/promises");
|
|
|
954
954
|
var import_node_fs4 = require("fs");
|
|
955
955
|
var import_node_path3 = require("path");
|
|
956
956
|
var import_node_url = require("url");
|
|
957
|
+
|
|
958
|
+
// src/integration-validation.ts
|
|
959
|
+
function validateIntegrations(cfg, path, defaultServices) {
|
|
960
|
+
if (cfg.integrations === void 0) return;
|
|
961
|
+
if (!Array.isArray(cfg.integrations)) throw new Error(`${path}: integrations must be an array`);
|
|
962
|
+
const ids = /* @__PURE__ */ new Set();
|
|
963
|
+
for (const [index, integration] of cfg.integrations.entries()) {
|
|
964
|
+
const at = `${path}: integrations[${index}]`;
|
|
965
|
+
if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
|
|
966
|
+
if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
|
|
967
|
+
if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
|
|
968
|
+
ids.add(integration.id);
|
|
969
|
+
if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
|
|
970
|
+
if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
|
|
971
|
+
if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
|
|
972
|
+
throw new Error(`${at}.schema must contain an entities object`);
|
|
973
|
+
}
|
|
974
|
+
if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
|
|
975
|
+
validateSeeds(integration, at);
|
|
976
|
+
validateProbes(integration, at);
|
|
977
|
+
}
|
|
978
|
+
const needsDb = cfg.integrations.some((integration) => integration.schema || integration.rules || integration.seeds?.length);
|
|
979
|
+
const services = unique(cfg.services?.length ? cfg.services : defaultServices);
|
|
980
|
+
if (needsDb && !services.includes("db")) throw new Error(`${path}: schema/rules/seed integrations require the db service`);
|
|
981
|
+
}
|
|
982
|
+
function validateSeeds(integration, at) {
|
|
983
|
+
if (integration.seeds === void 0) return;
|
|
984
|
+
if (!Array.isArray(integration.seeds)) throw new Error(`${at}.seeds must be an array`);
|
|
985
|
+
const ids = /* @__PURE__ */ new Set();
|
|
986
|
+
for (const [index, seed] of integration.seeds.entries()) {
|
|
987
|
+
const sat = `${at}.seeds[${index}]`;
|
|
988
|
+
if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
|
|
989
|
+
if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
|
|
990
|
+
ids.add(seed.id);
|
|
991
|
+
if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
|
|
992
|
+
throw new Error(`${sat}.key requires string attr and value`);
|
|
993
|
+
}
|
|
994
|
+
if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
|
|
995
|
+
if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
|
|
996
|
+
throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function validateProbes(integration, at) {
|
|
1001
|
+
if (integration.probes === void 0) return;
|
|
1002
|
+
if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
|
|
1003
|
+
for (const [index, probe] of integration.probes.entries()) {
|
|
1004
|
+
const pat = `${at}.probes[${index}]`;
|
|
1005
|
+
if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
|
|
1006
|
+
if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
|
|
1007
|
+
throw new Error(`${pat}.expectedStatus must be an HTTP status`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function isRecord4(value) {
|
|
1012
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1013
|
+
}
|
|
1014
|
+
function safeText(value, max) {
|
|
1015
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
1016
|
+
}
|
|
1017
|
+
function safeProbePath(value) {
|
|
1018
|
+
return typeof value === "string" && /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(value);
|
|
1019
|
+
}
|
|
1020
|
+
function validId(value) {
|
|
1021
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
1022
|
+
}
|
|
1023
|
+
function unique(values) {
|
|
1024
|
+
return [...new Set(values.filter(Boolean))];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/config.ts
|
|
957
1028
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
958
1029
|
var DEFAULT_ENVS = ["dev"];
|
|
959
1030
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
@@ -968,8 +1039,8 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
968
1039
|
validateRawConfig(raw, resolved);
|
|
969
1040
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
970
1041
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
971
|
-
const envs =
|
|
972
|
-
const services =
|
|
1042
|
+
const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
1043
|
+
const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
973
1044
|
validateCalendarConfig(raw, envs, services, resolved);
|
|
974
1045
|
const local = {
|
|
975
1046
|
tokenFile: (0, import_node_path3.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
@@ -1003,6 +1074,8 @@ async function resolveDataExport(cfg, value, names) {
|
|
|
1003
1074
|
throw new Error(`${value} did not export ${names.join(", ")} or default`);
|
|
1004
1075
|
}
|
|
1005
1076
|
function buildPlan(cfg) {
|
|
1077
|
+
const integrationSchema = cfg.integrations?.some((integration) => integration.schema) ?? false;
|
|
1078
|
+
const integrationRules = cfg.integrations?.some((integration) => integration.rules) ?? false;
|
|
1006
1079
|
return {
|
|
1007
1080
|
appId: cfg.app.id,
|
|
1008
1081
|
appName: cfg.app.name,
|
|
@@ -1010,8 +1083,9 @@ function buildPlan(cfg) {
|
|
|
1010
1083
|
dbEndpoint: cfg.dbEndpoint,
|
|
1011
1084
|
envs: cfg.envs,
|
|
1012
1085
|
services: cfg.services,
|
|
1013
|
-
|
|
1014
|
-
|
|
1086
|
+
integrations: (cfg.integrations ?? []).map((integration) => integration.id),
|
|
1087
|
+
hasSchema: !!cfg.db?.schema || integrationSchema,
|
|
1088
|
+
hasRules: !!cfg.db?.rules || integrationRules || (!!cfg.db?.schema || integrationSchema) && cfg.db?.defaultRules !== false,
|
|
1015
1089
|
aiProvider: cfg.ai?.provider
|
|
1016
1090
|
};
|
|
1017
1091
|
}
|
|
@@ -1020,7 +1094,7 @@ function calendarServiceConfig(cfg, env) {
|
|
|
1020
1094
|
if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
|
|
1021
1095
|
const google = cfg.calendar?.google;
|
|
1022
1096
|
if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
|
|
1023
|
-
const availability =
|
|
1097
|
+
const availability = unique2(
|
|
1024
1098
|
(google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
|
|
1025
1099
|
);
|
|
1026
1100
|
return {
|
|
@@ -1056,15 +1130,16 @@ function validateRawConfig(raw, path) {
|
|
|
1056
1130
|
if (!raw || typeof raw !== "object") throw new Error(`${path} must export an object`);
|
|
1057
1131
|
const cfg = raw;
|
|
1058
1132
|
if (!cfg.app || typeof cfg.app !== "object") throw new Error(`${path}: missing app object`);
|
|
1059
|
-
if (!
|
|
1133
|
+
if (!validId2(cfg.app.id)) throw new Error(`${path}: app.id must be lowercase letters, numbers, and hyphens`);
|
|
1060
1134
|
if (!cfg.app.name || typeof cfg.app.name !== "string") throw new Error(`${path}: app.name is required`);
|
|
1061
1135
|
if (cfg.envs !== void 0 && (!Array.isArray(cfg.envs) || cfg.envs.some((env) => typeof env !== "string"))) {
|
|
1062
1136
|
throw new Error(`${path}: envs must be an array of names`);
|
|
1063
1137
|
}
|
|
1064
|
-
if (cfg.envs?.some((env) => !
|
|
1138
|
+
if (cfg.envs?.some((env) => !validId2(env))) throw new Error(`${path}: env names must be lowercase letters, numbers, and hyphens`);
|
|
1065
1139
|
if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
|
|
1066
1140
|
throw new Error(`${path}: services must be an array of non-empty names`);
|
|
1067
1141
|
}
|
|
1142
|
+
validateIntegrations(cfg, path, DEFAULT_SERVICES);
|
|
1068
1143
|
}
|
|
1069
1144
|
function validateCalendarConfig(cfg, envs, services, path) {
|
|
1070
1145
|
const enabled = services.includes("calendar");
|
|
@@ -1072,9 +1147,9 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1072
1147
|
if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
|
|
1073
1148
|
return;
|
|
1074
1149
|
}
|
|
1075
|
-
if (!
|
|
1150
|
+
if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
|
|
1076
1151
|
assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
|
|
1077
|
-
if (!
|
|
1152
|
+
if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
1078
1153
|
const google = cfg.calendar.google;
|
|
1079
1154
|
assertOnly(
|
|
1080
1155
|
google,
|
|
@@ -1086,7 +1161,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1086
1161
|
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
1087
1162
|
}
|
|
1088
1163
|
const availability = google[availabilityKey];
|
|
1089
|
-
if (!
|
|
1164
|
+
if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
1090
1165
|
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
1091
1166
|
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
1092
1167
|
for (const env of envs) {
|
|
@@ -1097,22 +1172,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1097
1172
|
if (ids.length > 10) {
|
|
1098
1173
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
1099
1174
|
}
|
|
1100
|
-
if (ids.some((id) => !
|
|
1175
|
+
if (ids.some((id) => !safeText2(id, 1024))) {
|
|
1101
1176
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
1102
1177
|
}
|
|
1103
1178
|
}
|
|
1104
1179
|
if (google.bookingCalendar !== void 0) {
|
|
1105
|
-
if (!
|
|
1180
|
+
if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
1106
1181
|
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
1107
1182
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
1108
1183
|
for (const [env, value] of Object.entries(google.bookingCalendar)) {
|
|
1109
|
-
if (!
|
|
1184
|
+
if (!safeText2(value, 1024)) {
|
|
1110
1185
|
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
1111
1186
|
}
|
|
1112
1187
|
}
|
|
1113
1188
|
}
|
|
1114
1189
|
if (google.bookingPageUrl !== void 0) {
|
|
1115
|
-
if (!
|
|
1190
|
+
if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
|
|
1116
1191
|
const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
|
|
1117
1192
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
|
|
1118
1193
|
for (const [env, value] of Object.entries(google.bookingPageUrl)) {
|
|
@@ -1127,10 +1202,10 @@ function assertOnly(value, allowed, label) {
|
|
|
1127
1202
|
const extra = Object.keys(value).find((key) => !allowed.includes(key));
|
|
1128
1203
|
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
1129
1204
|
}
|
|
1130
|
-
function
|
|
1205
|
+
function isRecord5(value) {
|
|
1131
1206
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1132
1207
|
}
|
|
1133
|
-
function
|
|
1208
|
+
function safeText2(value, max) {
|
|
1134
1209
|
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
1135
1210
|
}
|
|
1136
1211
|
function safeHttpsUrl(value) {
|
|
@@ -1142,7 +1217,7 @@ function safeHttpsUrl(value) {
|
|
|
1142
1217
|
return false;
|
|
1143
1218
|
}
|
|
1144
1219
|
}
|
|
1145
|
-
function
|
|
1220
|
+
function validId2(value) {
|
|
1146
1221
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
1147
1222
|
}
|
|
1148
1223
|
async function loadConfigModule(path) {
|
|
@@ -1155,7 +1230,7 @@ async function loadConfigModule(path) {
|
|
|
1155
1230
|
function trimSlash(value) {
|
|
1156
1231
|
return value.replace(/\/+$/, "");
|
|
1157
1232
|
}
|
|
1158
|
-
function
|
|
1233
|
+
function unique2(values) {
|
|
1159
1234
|
return [...new Set(values.filter(Boolean))];
|
|
1160
1235
|
}
|
|
1161
1236
|
|
|
@@ -1285,9 +1360,9 @@ var CAPABILITIES = {
|
|
|
1285
1360
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
1286
1361
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1287
1362
|
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1288
|
-
"
|
|
1363
|
+
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
1289
1364
|
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
1290
|
-
"validate
|
|
1365
|
+
"validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
|
|
1291
1366
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
1292
1367
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
1293
1368
|
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
@@ -1295,7 +1370,7 @@ var CAPABILITIES = {
|
|
|
1295
1370
|
agent: [
|
|
1296
1371
|
"install and import the selected odla SDKs",
|
|
1297
1372
|
"wrap the Worker with withObservability and choose useful telemetry",
|
|
1298
|
-
"make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1373
|
+
"install capability packages, mount their runtime routes, and make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1299
1374
|
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
1300
1375
|
],
|
|
1301
1376
|
human: [
|
|
@@ -1967,20 +2042,121 @@ function readPackageJson(rootDir) {
|
|
|
1967
2042
|
}
|
|
1968
2043
|
}
|
|
1969
2044
|
|
|
2045
|
+
// src/integrations.ts
|
|
2046
|
+
var import_node_util = require("util");
|
|
2047
|
+
async function resolveDatabaseConfig(cfg, options = {}) {
|
|
2048
|
+
const integrations = cfg.integrations ?? [];
|
|
2049
|
+
if (!cfg.services.includes("db")) return { schema: void 0, rules: void 0, integrations };
|
|
2050
|
+
const authoredSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
2051
|
+
const schema = mergeSchemas(authoredSchema, integrations);
|
|
2052
|
+
if (options.validateSeeds !== false) assertSeedContracts(integrations, schema);
|
|
2053
|
+
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
2054
|
+
const explicitRules = mergeRules(configuredRules, integrations);
|
|
2055
|
+
const rules = configuredRules === void 0 && schema && cfg.db?.defaultRules !== false ? { ...rulesFromSchema(schema), ...explicitRules ?? {} } : explicitRules;
|
|
2056
|
+
return { schema, rules, integrations };
|
|
2057
|
+
}
|
|
2058
|
+
function integrationWarnings(integrations, schema, rules) {
|
|
2059
|
+
const warnings = [];
|
|
2060
|
+
const entities = new Set(serializedEntities(schema));
|
|
2061
|
+
for (const integration of integrations) {
|
|
2062
|
+
const namespaces = Object.keys(integration.schema?.entities ?? {});
|
|
2063
|
+
for (const ns of namespaces) {
|
|
2064
|
+
if (!entities.has(ns)) warnings.push(`integration "${integration.id}" schema namespace "${ns}" is missing`);
|
|
2065
|
+
const rule = rules?.[ns];
|
|
2066
|
+
if (!rule) {
|
|
2067
|
+
warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
for (const action of ["view", "create", "update", "delete"]) {
|
|
2071
|
+
if (rule[action] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action}" is missing`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
for (const seed of integration.seeds ?? []) {
|
|
2075
|
+
if (!entities.has(seed.ns)) {
|
|
2076
|
+
warnings.push(`integration "${integration.id}" seed "${seed.id}" targets unknown namespace "${seed.ns}"`);
|
|
2077
|
+
} else if (!isUniqueAttr(schema, seed.ns, seed.key.attr)) {
|
|
2078
|
+
warnings.push(`integration "${integration.id}" seed "${seed.id}" key "${seed.ns}.${seed.key.attr}" is not declared unique`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return warnings;
|
|
2083
|
+
}
|
|
2084
|
+
function mergeSchemas(base, integrations) {
|
|
2085
|
+
const fragments = integrations.filter((integration) => integration.schema);
|
|
2086
|
+
if (fragments.length === 0) return base;
|
|
2087
|
+
const merged = normalizeSchema(base);
|
|
2088
|
+
for (const integration of fragments) {
|
|
2089
|
+
const fragment = normalizeSchema(integration.schema);
|
|
2090
|
+
mergeMap(merged.entities, fragment.entities, `integration "${integration.id}" schema namespace`);
|
|
2091
|
+
mergeMap(merged.links, fragment.links, `integration "${integration.id}" schema link`);
|
|
2092
|
+
}
|
|
2093
|
+
return merged;
|
|
2094
|
+
}
|
|
2095
|
+
function mergeRules(base, integrations) {
|
|
2096
|
+
const fragments = integrations.filter((integration) => integration.rules);
|
|
2097
|
+
if (fragments.length === 0) return base;
|
|
2098
|
+
const merged = { ...base ?? {} };
|
|
2099
|
+
for (const integration of fragments) {
|
|
2100
|
+
mergeMap(merged, integration.rules ?? {}, `integration "${integration.id}" rule namespace`);
|
|
2101
|
+
}
|
|
2102
|
+
return merged;
|
|
2103
|
+
}
|
|
2104
|
+
function assertSeedContracts(integrations, schema) {
|
|
2105
|
+
const entities = new Set(serializedEntities(schema));
|
|
2106
|
+
for (const integration of integrations) {
|
|
2107
|
+
for (const seed of integration.seeds ?? []) {
|
|
2108
|
+
if (!entities.has(seed.ns)) {
|
|
2109
|
+
throw new Error(`integration "${integration.id}" seed "${seed.id}" targets unknown namespace "${seed.ns}"`);
|
|
2110
|
+
}
|
|
2111
|
+
if (!isUniqueAttr(schema, seed.ns, seed.key.attr)) {
|
|
2112
|
+
throw new Error(`integration "${integration.id}" seed "${seed.id}" key "${seed.ns}.${seed.key.attr}" must be declared unique`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
function isUniqueAttr(schema, ns, attr) {
|
|
2118
|
+
if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
|
|
2119
|
+
const entity = schema.entities[ns];
|
|
2120
|
+
if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
|
|
2121
|
+
const definition = entity.attrs[attr];
|
|
2122
|
+
return isRecord6(definition) && definition.unique === true;
|
|
2123
|
+
}
|
|
2124
|
+
function normalizeSchema(value) {
|
|
2125
|
+
if (value === void 0 || value === null) return { entities: {}, links: {} };
|
|
2126
|
+
if (!isRecord6(value) || !isRecord6(value.entities)) {
|
|
2127
|
+
throw new Error("db schema must be a serialized schema object with an entities map");
|
|
2128
|
+
}
|
|
2129
|
+
if (value.links !== void 0 && !isRecord6(value.links)) throw new Error("db schema links must be an object");
|
|
2130
|
+
return {
|
|
2131
|
+
entities: { ...value.entities },
|
|
2132
|
+
links: { ...value.links ?? {} }
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function mergeMap(target, fragment, label) {
|
|
2136
|
+
for (const [name, value] of Object.entries(fragment)) {
|
|
2137
|
+
if (Object.hasOwn(target, name) && !(0, import_node_util.isDeepStrictEqual)(target[name], value)) {
|
|
2138
|
+
throw new Error(`${label} "${name}" conflicts with an existing definition`);
|
|
2139
|
+
}
|
|
2140
|
+
target[name] = value;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
function isRecord6(value) {
|
|
2144
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
1970
2147
|
// src/doctor.ts
|
|
1971
2148
|
async function doctor(options) {
|
|
1972
2149
|
const out = options.stdout ?? console;
|
|
1973
2150
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1974
2151
|
const plan = buildPlan(cfg);
|
|
1975
|
-
const
|
|
1976
|
-
const
|
|
1977
|
-
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
1978
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
2152
|
+
const database = await resolveDatabaseConfig(cfg, { validateSeeds: false });
|
|
2153
|
+
const { schema, rules } = database;
|
|
1979
2154
|
const entities = serializedEntities(schema);
|
|
1980
2155
|
out.log(`config: ${cfg.configPath}`);
|
|
1981
2156
|
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
1982
2157
|
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
1983
2158
|
out.log(`svc: ${plan.services.join(", ")}`);
|
|
2159
|
+
out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
1984
2160
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
1985
2161
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
1986
2162
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
@@ -2002,6 +2178,7 @@ async function doctor(options) {
|
|
|
2002
2178
|
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
2003
2179
|
}
|
|
2004
2180
|
}
|
|
2181
|
+
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
2005
2182
|
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
2006
2183
|
if (cfg.auth?.clerk) {
|
|
2007
2184
|
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
@@ -2102,8 +2279,8 @@ Commands:
|
|
|
2102
2279
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
2103
2280
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
2104
2281
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
2105
|
-
provision Register services,
|
|
2106
|
-
smoke Verify
|
|
2282
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
2283
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
2107
2284
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
2108
2285
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
2109
2286
|
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
@@ -2213,6 +2390,9 @@ function configTemplate(input) {
|
|
|
2213
2390
|
},
|
|
2214
2391
|
envs: ${JSON.stringify(input.envs)},
|
|
2215
2392
|
services: ${JSON.stringify(input.services)},
|
|
2393
|
+
// App capabilities are npm modules, not hosted services. Import their
|
|
2394
|
+
// data-only descriptor and add it here (for example createCrmIntegration()).
|
|
2395
|
+
integrations: [],
|
|
2216
2396
|
db: {
|
|
2217
2397
|
schema: "./src/odla/schema.mjs",
|
|
2218
2398
|
rules: "./src/odla/rules.mjs",
|
|
@@ -2290,9 +2470,61 @@ function relativeDisplay(path, rootDir) {
|
|
|
2290
2470
|
|
|
2291
2471
|
// src/provision.ts
|
|
2292
2472
|
var import_apps2 = require("@odla-ai/apps");
|
|
2293
|
-
var
|
|
2473
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
2294
2474
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
2295
2475
|
|
|
2476
|
+
// src/integration-provision.ts
|
|
2477
|
+
var import_db3 = require("@odla-ai/db");
|
|
2478
|
+
async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, integrations, env, out) {
|
|
2479
|
+
const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
|
|
2480
|
+
for (const integration of integrations) {
|
|
2481
|
+
for (const seed of integration.seeds ?? []) {
|
|
2482
|
+
const payload = await postJson(doFetch, `${base}/query`, dbKey, {
|
|
2483
|
+
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
2484
|
+
});
|
|
2485
|
+
const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
|
|
2486
|
+
if (!Array.isArray(rows)) {
|
|
2487
|
+
throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
|
|
2488
|
+
}
|
|
2489
|
+
if (rows.length > 0) {
|
|
2490
|
+
out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
await postJson(doFetch, `${base}/transact`, dbKey, {
|
|
2494
|
+
mutationId: `integration:${integration.id}:seed:${seed.id}`,
|
|
2495
|
+
ops: [{
|
|
2496
|
+
t: "update",
|
|
2497
|
+
ns: seed.ns,
|
|
2498
|
+
// A fresh id plus the declared-unique natural key is create-only: a
|
|
2499
|
+
// concurrent creator gets a unique violation instead of being overwritten.
|
|
2500
|
+
id: (0, import_db3.uuidv7)(),
|
|
2501
|
+
attrs: { ...seed.attrs, [seed.key.attr]: seed.key.value }
|
|
2502
|
+
}]
|
|
2503
|
+
});
|
|
2504
|
+
out.log(`${env}: integration ${integration.id} seed ${seed.id} created`);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
async function postJson(doFetch, url, bearer, body) {
|
|
2509
|
+
const res = await doFetch(url, {
|
|
2510
|
+
method: "POST",
|
|
2511
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2512
|
+
body: JSON.stringify(body)
|
|
2513
|
+
});
|
|
2514
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
|
|
2515
|
+
return res.json().catch(() => ({}));
|
|
2516
|
+
}
|
|
2517
|
+
function isRecord7(value) {
|
|
2518
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2519
|
+
}
|
|
2520
|
+
async function responseText(res) {
|
|
2521
|
+
try {
|
|
2522
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
2523
|
+
} catch {
|
|
2524
|
+
return "";
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2296
2528
|
// src/provision-credentials.ts
|
|
2297
2529
|
var import_apps = require("@odla-ai/apps");
|
|
2298
2530
|
async function provisionEnvCredentials(opts) {
|
|
@@ -2352,14 +2584,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
2352
2584
|
appId: tenantId
|
|
2353
2585
|
})
|
|
2354
2586
|
});
|
|
2355
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
2587
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText3(created)}`);
|
|
2356
2588
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
2357
2589
|
method: "POST",
|
|
2358
2590
|
headers,
|
|
2359
2591
|
body: "{}"
|
|
2360
2592
|
});
|
|
2361
2593
|
}
|
|
2362
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
2594
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2363
2595
|
const body = await res.json();
|
|
2364
2596
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
2365
2597
|
return body.key;
|
|
@@ -2375,12 +2607,12 @@ async function issueO11yToken(opts) {
|
|
|
2375
2607
|
`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`
|
|
2376
2608
|
);
|
|
2377
2609
|
}
|
|
2378
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
2610
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2379
2611
|
const body = await res.json();
|
|
2380
2612
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
2381
2613
|
return body.token;
|
|
2382
2614
|
}
|
|
2383
|
-
async function
|
|
2615
|
+
async function safeText3(res) {
|
|
2384
2616
|
try {
|
|
2385
2617
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
2386
2618
|
} catch {
|
|
@@ -2388,6 +2620,18 @@ async function safeText2(res) {
|
|
|
2388
2620
|
}
|
|
2389
2621
|
}
|
|
2390
2622
|
|
|
2623
|
+
// src/provision-helpers.ts
|
|
2624
|
+
var import_ai = require("@odla-ai/ai");
|
|
2625
|
+
function serviceRank(service) {
|
|
2626
|
+
if (service === "db") return 0;
|
|
2627
|
+
if (service === "calendar") return 2;
|
|
2628
|
+
return 1;
|
|
2629
|
+
}
|
|
2630
|
+
function defaultSecretName(provider) {
|
|
2631
|
+
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
2632
|
+
return names[provider] ?? `${provider}_api_key`;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2391
2635
|
// src/secrets.ts
|
|
2392
2636
|
var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
2393
2637
|
async function secretsPush(options) {
|
|
@@ -2479,19 +2723,23 @@ async function provision(options) {
|
|
|
2479
2723
|
out.log(` db: ${plan.dbEndpoint}`);
|
|
2480
2724
|
out.log(` envs: ${plan.envs.join(", ")}`);
|
|
2481
2725
|
out.log(` services: ${plan.services.join(", ")}`);
|
|
2726
|
+
out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
2482
2727
|
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
2483
2728
|
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
2484
2729
|
throw new Error(
|
|
2485
2730
|
`refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
|
|
2486
2731
|
);
|
|
2487
2732
|
}
|
|
2488
|
-
const
|
|
2489
|
-
const
|
|
2490
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
2733
|
+
const database = await resolveDatabaseConfig(cfg);
|
|
2734
|
+
const { schema, rules } = database;
|
|
2491
2735
|
if (options.dryRun) {
|
|
2492
2736
|
out.log("dry run: no network calls or file writes");
|
|
2493
2737
|
out.log(` schema: ${schema ? "yes" : "no"}`);
|
|
2494
2738
|
out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
|
|
2739
|
+
for (const integration of database.integrations) {
|
|
2740
|
+
const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
|
|
2741
|
+
out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
|
|
2742
|
+
}
|
|
2495
2743
|
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
2496
2744
|
if (cfg.services.includes("calendar")) {
|
|
2497
2745
|
for (const env of cfg.envs) {
|
|
@@ -2601,18 +2849,21 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2601
2849
|
}
|
|
2602
2850
|
}
|
|
2603
2851
|
if (schema && dbKey) {
|
|
2604
|
-
await
|
|
2852
|
+
await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
2605
2853
|
out.log(`${env}: schema pushed`);
|
|
2606
2854
|
}
|
|
2607
2855
|
if (rules) {
|
|
2608
|
-
await
|
|
2856
|
+
await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
2609
2857
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
2610
2858
|
}
|
|
2859
|
+
if (dbKey) {
|
|
2860
|
+
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
2861
|
+
}
|
|
2611
2862
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
2612
2863
|
const key = import_node_process7.default.env[cfg.ai.keyEnv];
|
|
2613
2864
|
if (key) {
|
|
2614
2865
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
2615
|
-
await (0,
|
|
2866
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
2616
2867
|
out.log(`${env}: ${cfg.ai.provider} key stored in vault (${secretName})`);
|
|
2617
2868
|
} else {
|
|
2618
2869
|
out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
|
|
@@ -2648,11 +2899,6 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2648
2899
|
}
|
|
2649
2900
|
}
|
|
2650
2901
|
}
|
|
2651
|
-
function serviceRank(service) {
|
|
2652
|
-
if (service === "db") return 0;
|
|
2653
|
-
if (service === "calendar") return 2;
|
|
2654
|
-
return 1;
|
|
2655
|
-
}
|
|
2656
2902
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
2657
2903
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
2658
2904
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -2662,13 +2908,13 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
2662
2908
|
const json = await res.json();
|
|
2663
2909
|
return json.app ?? null;
|
|
2664
2910
|
}
|
|
2665
|
-
async function
|
|
2911
|
+
async function postJson2(doFetch, url, bearer, body) {
|
|
2666
2912
|
const res = await doFetch(url, {
|
|
2667
2913
|
method: "POST",
|
|
2668
2914
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2669
2915
|
body: JSON.stringify(body)
|
|
2670
2916
|
});
|
|
2671
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
2917
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
|
|
2672
2918
|
}
|
|
2673
2919
|
function normalizeClerkConfig(value) {
|
|
2674
2920
|
if (!value) return null;
|
|
@@ -2681,11 +2927,7 @@ function normalizeClerkConfig(value) {
|
|
|
2681
2927
|
const publishableKey = envValue(cfg.publishableKey);
|
|
2682
2928
|
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
2683
2929
|
}
|
|
2684
|
-
function
|
|
2685
|
-
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
2686
|
-
return names[provider] ?? `${provider}_api_key`;
|
|
2687
|
-
}
|
|
2688
|
-
async function safeText3(res) {
|
|
2930
|
+
async function safeText4(res) {
|
|
2689
2931
|
try {
|
|
2690
2932
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
2691
2933
|
} catch {
|
|
@@ -2694,7 +2936,7 @@ async function safeText3(res) {
|
|
|
2694
2936
|
}
|
|
2695
2937
|
|
|
2696
2938
|
// src/secrets-set.ts
|
|
2697
|
-
var
|
|
2939
|
+
var import_ai3 = require("@odla-ai/ai");
|
|
2698
2940
|
var import_apps3 = require("@odla-ai/apps");
|
|
2699
2941
|
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
2700
2942
|
async function secretsSet(options) {
|
|
@@ -2706,7 +2948,7 @@ async function secretsSet(options) {
|
|
|
2706
2948
|
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
2707
2949
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
2708
2950
|
try {
|
|
2709
|
-
await (0,
|
|
2951
|
+
await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
|
|
2710
2952
|
} catch (err) {
|
|
2711
2953
|
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
|
|
2712
2954
|
}
|
|
@@ -3012,7 +3254,7 @@ function formatBudget(usage) {
|
|
|
3012
3254
|
|
|
3013
3255
|
// src/security-hosted-github.ts
|
|
3014
3256
|
var import_node_child_process4 = require("child_process");
|
|
3015
|
-
var
|
|
3257
|
+
var import_node_util2 = require("util");
|
|
3016
3258
|
|
|
3017
3259
|
// src/security-hosted-request.ts
|
|
3018
3260
|
async function requestHostedSecurityJson(options, path, init, action) {
|
|
@@ -3216,7 +3458,7 @@ async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultRe
|
|
|
3216
3458
|
return repositoryFromGitRemote(remote);
|
|
3217
3459
|
}
|
|
3218
3460
|
async function defaultReadOrigin(cwd) {
|
|
3219
|
-
const result = await (0,
|
|
3461
|
+
const result = await (0, import_node_util2.promisify)(import_node_child_process4.execFile)(
|
|
3220
3462
|
"git",
|
|
3221
3463
|
["remote", "get-url", "origin"],
|
|
3222
3464
|
{ cwd, encoding: "utf8" }
|
|
@@ -3954,7 +4196,8 @@ async function smoke(options) {
|
|
|
3954
4196
|
assertCalendarHealthy(status, calendarServiceConfig(cfg, env));
|
|
3955
4197
|
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
3956
4198
|
}
|
|
3957
|
-
const
|
|
4199
|
+
const database = await resolveDatabaseConfig(cfg);
|
|
4200
|
+
const expectedSchema = database.schema;
|
|
3958
4201
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
3959
4202
|
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
|
|
3960
4203
|
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
@@ -3966,7 +4209,7 @@ async function smoke(options) {
|
|
|
3966
4209
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
3967
4210
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
3968
4211
|
if (aggregateEntity) {
|
|
3969
|
-
const aggregate = await
|
|
4212
|
+
const aggregate = await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3970
4213
|
ns: aggregateEntity,
|
|
3971
4214
|
aggregate: { count: true }
|
|
3972
4215
|
});
|
|
@@ -3975,6 +4218,21 @@ async function smoke(options) {
|
|
|
3975
4218
|
} else {
|
|
3976
4219
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
3977
4220
|
}
|
|
4221
|
+
const probes = database.integrations.flatMap(
|
|
4222
|
+
(integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
|
|
4223
|
+
);
|
|
4224
|
+
if (probes.length) {
|
|
4225
|
+
const link = cfg.links?.[env];
|
|
4226
|
+
if (!link) throw new Error(`integration smoke probes require links.${env} in odla.config.mjs`);
|
|
4227
|
+
for (const { integration, probe } of probes) {
|
|
4228
|
+
const probeUrl = new URL(probe.path, link).toString();
|
|
4229
|
+
const res = await doFetch(probeUrl, { redirect: "manual" });
|
|
4230
|
+
if (res.status !== probe.expectedStatus) {
|
|
4231
|
+
throw new Error(`integration "${integration}" probe ${probe.path} returned ${res.status}; expected ${probe.expectedStatus}`);
|
|
4232
|
+
}
|
|
4233
|
+
out.log(` integration.${integration}: ${probe.path} \u2192 ${res.status}`);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
3978
4236
|
out.log("ok");
|
|
3979
4237
|
}
|
|
3980
4238
|
function assertCalendarHealthy(status, expected) {
|
|
@@ -3992,16 +4250,16 @@ async function getJson(doFetch, url, bearer) {
|
|
|
3992
4250
|
const res = await doFetch(url, {
|
|
3993
4251
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
3994
4252
|
});
|
|
3995
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4253
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
3996
4254
|
return res.json();
|
|
3997
4255
|
}
|
|
3998
|
-
async function
|
|
4256
|
+
async function postJson3(doFetch, url, bearer, body) {
|
|
3999
4257
|
const res = await doFetch(url, {
|
|
4000
4258
|
method: "POST",
|
|
4001
4259
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
4002
4260
|
body: JSON.stringify(body)
|
|
4003
4261
|
});
|
|
4004
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4262
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
4005
4263
|
return res.json();
|
|
4006
4264
|
}
|
|
4007
4265
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -4009,7 +4267,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
4009
4267
|
url.searchParams.set("env", env);
|
|
4010
4268
|
return url.toString();
|
|
4011
4269
|
}
|
|
4012
|
-
async function
|
|
4270
|
+
async function safeText5(res) {
|
|
4013
4271
|
try {
|
|
4014
4272
|
return (await res.text()).slice(0, 500);
|
|
4015
4273
|
} catch {
|