@odla-ai/cli 0.32.1 → 0.33.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/dist/index.cjs CHANGED
@@ -1075,6 +1075,111 @@ function safeText(value2, max) {
1075
1075
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1076
1076
  }
1077
1077
 
1078
+ // src/calendar-config.ts
1079
+ function calendarServiceConfig(cfg, env) {
1080
+ if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1081
+ if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
1082
+ const google = cfg.calendar?.google;
1083
+ if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
1084
+ const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1085
+ if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1086
+ const availability = unique(configured.map((id) => id.trim()));
1087
+ return {
1088
+ provider: "google",
1089
+ access: "book",
1090
+ bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
1091
+ availabilityCalendars: availability
1092
+ };
1093
+ }
1094
+ function calendarBookingPageUrl(cfg, env) {
1095
+ const value2 = cfg.calendar?.google.bookingPageUrl?.[env];
1096
+ if (value2 === void 0 || value2 === null) return value2;
1097
+ return new URL(value2).toString();
1098
+ }
1099
+ function validateCalendarConfig(cfg, envs, services, path) {
1100
+ const enabled = services.includes("calendar");
1101
+ if (!cfg.calendar) {
1102
+ if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1103
+ return;
1104
+ }
1105
+ if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1106
+ assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1107
+ if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1108
+ const google = cfg.calendar.google;
1109
+ assertOnly2(
1110
+ google,
1111
+ ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1112
+ `${path}: calendar.google`
1113
+ );
1114
+ const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
1115
+ if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
1116
+ throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1117
+ }
1118
+ const availability = google[availabilityKey];
1119
+ if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1120
+ const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1121
+ if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1122
+ for (const env of envs) {
1123
+ const ids = availability[env];
1124
+ if (!Array.isArray(ids) || ids.length === 0) {
1125
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1126
+ }
1127
+ }
1128
+ for (const [env, ids] of Object.entries(availability)) {
1129
+ if (!Array.isArray(ids) || ids.length === 0) {
1130
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1131
+ }
1132
+ if (ids.length > 10) {
1133
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1134
+ }
1135
+ if (ids.some((id) => !safeText2(id, 1024))) {
1136
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1137
+ }
1138
+ }
1139
+ if (google.bookingCalendar !== void 0) {
1140
+ if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1141
+ const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1142
+ if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1143
+ for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1144
+ if (!safeText2(value2, 1024)) {
1145
+ throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1146
+ }
1147
+ }
1148
+ }
1149
+ if (google.bookingPageUrl !== void 0) {
1150
+ if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1151
+ const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1152
+ if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1153
+ for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1154
+ if (value2 !== null && !safeHttpsUrl(value2)) {
1155
+ throw new Error(`${path}: calendar.google.bookingPageUrl.${env} must be an HTTPS URL without credentials or fragment`);
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ function assertOnly2(value2, allowed, label) {
1161
+ const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1162
+ if (extra) throw new Error(`${label}.${extra} is not supported`);
1163
+ }
1164
+ function isRecord5(value2) {
1165
+ return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1166
+ }
1167
+ function safeText2(value2, max) {
1168
+ return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1169
+ }
1170
+ function safeHttpsUrl(value2) {
1171
+ if (typeof value2 !== "string" || value2.length > 2048) return false;
1172
+ try {
1173
+ const url = new URL(value2);
1174
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
1175
+ } catch {
1176
+ return false;
1177
+ }
1178
+ }
1179
+ function unique(values) {
1180
+ return [...new Set(values.filter(Boolean))];
1181
+ }
1182
+
1078
1183
  // src/integration-validation.ts
1079
1184
  function validateIntegrations(cfg, path, defaultServices) {
1080
1185
  if (cfg.integrations === void 0) return;
@@ -1082,36 +1187,61 @@ function validateIntegrations(cfg, path, defaultServices) {
1082
1187
  const ids = /* @__PURE__ */ new Set();
1083
1188
  for (const [index, integration] of cfg.integrations.entries()) {
1084
1189
  const at = `${path}: integrations[${index}]`;
1085
- if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
1190
+ if (!isRecord6(integration)) throw new Error(`${at} must be an object`);
1086
1191
  if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
1087
1192
  if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
1088
1193
  ids.add(integration.id);
1089
- if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
1090
- if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1091
- if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
1194
+ if (!safeText3(integration.title, 200)) throw new Error(`${at}.title is required`);
1195
+ if (!safeText3(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1196
+ if (integration.schema !== void 0 && (!isRecord6(integration.schema) || !isRecord6(integration.schema.entities))) {
1092
1197
  throw new Error(`${at}.schema must contain an entities object`);
1093
1198
  }
1094
- if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
1199
+ if (integration.rules !== void 0 && !isRecord6(integration.rules)) throw new Error(`${at}.rules must be an object`);
1095
1200
  validateSeeds(integration, at);
1096
1201
  validateProbes(integration, at);
1202
+ validateSecrets(integration.secrets, at);
1097
1203
  }
1098
1204
  const needsDb = cfg.integrations.some((integration) => integration.schema || integration.rules || integration.seeds?.length);
1099
- const services = unique(cfg.services?.length ? cfg.services : defaultServices);
1205
+ const services = unique2(cfg.services?.length ? cfg.services : defaultServices);
1100
1206
  if (needsDb && !services.includes("db")) throw new Error(`${path}: schema/rules/seed integrations require the db service`);
1101
1207
  }
1208
+ var SECRET_NAME = /^\$?[a-z][a-z0-9_]*$/;
1209
+ function validateSecrets(value2, at) {
1210
+ if (value2 === void 0) return;
1211
+ if (!Array.isArray(value2)) throw new Error(`${at}.secrets must be an array`);
1212
+ const names = /* @__PURE__ */ new Set();
1213
+ for (const [index, secret] of value2.entries()) {
1214
+ const sat = `${at}.secrets[${index}]`;
1215
+ if (!isRecord6(secret)) throw new Error(`${sat} must be an object`);
1216
+ if (typeof secret.name !== "string" || !SECRET_NAME.test(secret.name) || secret.name.length > 64) {
1217
+ throw new Error(`${sat}.name must be lowercase snake_case (optionally "$"-prefixed when reserved), e.g. "clerk_webhook_secret"`);
1218
+ }
1219
+ const dollar = secret.name.startsWith("$");
1220
+ if (dollar !== (secret.reserved === true)) {
1221
+ throw new Error(
1222
+ dollar ? `${sat}.name is "$"-prefixed, so it must also set reserved: true` : `${sat} sets reserved: true, so its name must be "$"-prefixed`
1223
+ );
1224
+ }
1225
+ if (!safeText3(secret.description, 500)) throw new Error(`${sat}.description is required \u2014 it is what doctor and the docs show`);
1226
+ if (secret.pattern !== void 0 && !safeText3(secret.pattern, 64)) throw new Error(`${sat}.pattern must be a non-empty prefix string`);
1227
+ if (secret.required !== void 0 && typeof secret.required !== "boolean") throw new Error(`${sat}.required must be a boolean`);
1228
+ if (names.has(secret.name)) throw new Error(`${at} declares secret "${secret.name}" twice`);
1229
+ names.add(secret.name);
1230
+ }
1231
+ }
1102
1232
  function validateSeeds(integration, at) {
1103
1233
  if (integration.seeds === void 0) return;
1104
1234
  if (!Array.isArray(integration.seeds)) throw new Error(`${at}.seeds must be an array`);
1105
1235
  const ids = /* @__PURE__ */ new Set();
1106
1236
  for (const [index, seed] of integration.seeds.entries()) {
1107
1237
  const sat = `${at}.seeds[${index}]`;
1108
- if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1238
+ if (!isRecord6(seed) || !safeText3(seed.id, 200) || !safeText3(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1109
1239
  if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
1110
1240
  ids.add(seed.id);
1111
- if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
1241
+ if (!isRecord6(seed.key) || !safeText3(seed.key.attr, 200) || !safeText3(seed.key.value, 2048)) {
1112
1242
  throw new Error(`${sat}.key requires string attr and value`);
1113
1243
  }
1114
- if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1244
+ if (!isRecord6(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1115
1245
  if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
1116
1246
  throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
1117
1247
  }
@@ -1122,16 +1252,16 @@ function validateProbes(integration, at) {
1122
1252
  if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
1123
1253
  for (const [index, probe] of integration.probes.entries()) {
1124
1254
  const pat = `${at}.probes[${index}]`;
1125
- if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1255
+ if (!isRecord6(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1126
1256
  if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
1127
1257
  throw new Error(`${pat}.expectedStatus must be an HTTP status`);
1128
1258
  }
1129
1259
  }
1130
1260
  }
1131
- function isRecord5(value2) {
1261
+ function isRecord6(value2) {
1132
1262
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1133
1263
  }
1134
- function safeText2(value2, max) {
1264
+ function safeText3(value2, max) {
1135
1265
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1136
1266
  }
1137
1267
  function safeProbePath(value2) {
@@ -1140,7 +1270,7 @@ function safeProbePath(value2) {
1140
1270
  function validId(value2) {
1141
1271
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1142
1272
  }
1143
- function unique(values) {
1273
+ function unique2(values) {
1144
1274
  return [...new Set(values.filter(Boolean))];
1145
1275
  }
1146
1276
 
@@ -1160,10 +1290,10 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1160
1290
  validateRawConfig(raw, resolved);
1161
1291
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1162
1292
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
1163
- const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1164
- const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1293
+ const envs = unique3(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1294
+ const services = unique3(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1165
1295
  validateServices(services, resolved);
1166
- validateCalendarConfig(raw, unique2([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1296
+ validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1167
1297
  const local = {
1168
1298
  tokenFile: (0, import_node_path4.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1169
1299
  credentialsFile: (0, import_node_path4.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
@@ -1211,26 +1341,6 @@ function buildPlan(cfg) {
1211
1341
  aiProvider: cfg.ai?.provider
1212
1342
  };
1213
1343
  }
1214
- function calendarServiceConfig(cfg, env) {
1215
- if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1216
- if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
1217
- const google = cfg.calendar?.google;
1218
- if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
1219
- const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1220
- if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1221
- const availability = unique2(configured.map((id) => id.trim()));
1222
- return {
1223
- provider: "google",
1224
- access: "book",
1225
- bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
1226
- availabilityCalendars: availability
1227
- };
1228
- }
1229
- function calendarBookingPageUrl(cfg, env) {
1230
- const value2 = cfg.calendar?.google.bookingPageUrl?.[env];
1231
- if (value2 === void 0 || value2 === null) return value2;
1232
- return new URL(value2).toString();
1233
- }
1234
1344
  function rulesFromSchema(schema) {
1235
1345
  const entities = serializedEntities(schema);
1236
1346
  return Object.fromEntries(
@@ -1262,69 +1372,9 @@ function validateRawConfig(raw, path) {
1262
1372
  throw new Error(`${path}: services must be an array of non-empty names`);
1263
1373
  }
1264
1374
  validateAiConfig(cfg, path);
1375
+ validateSecrets(cfg.secrets, `${path}: config`);
1265
1376
  validateIntegrations(cfg, path, DEFAULT_SERVICES);
1266
1377
  }
1267
- function validateCalendarConfig(cfg, envs, services, path) {
1268
- const enabled = services.includes("calendar");
1269
- if (!cfg.calendar) {
1270
- if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1271
- return;
1272
- }
1273
- if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1274
- assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1275
- if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1276
- const google = cfg.calendar.google;
1277
- assertOnly2(
1278
- google,
1279
- ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1280
- `${path}: calendar.google`
1281
- );
1282
- const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
1283
- if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
1284
- throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1285
- }
1286
- const availability = google[availabilityKey];
1287
- if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1288
- const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1289
- if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1290
- for (const env of envs) {
1291
- const ids = availability[env];
1292
- if (!Array.isArray(ids) || ids.length === 0) {
1293
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1294
- }
1295
- }
1296
- for (const [env, ids] of Object.entries(availability)) {
1297
- if (!Array.isArray(ids) || ids.length === 0) {
1298
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1299
- }
1300
- if (ids.length > 10) {
1301
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1302
- }
1303
- if (ids.some((id) => !safeText3(id, 1024))) {
1304
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1305
- }
1306
- }
1307
- if (google.bookingCalendar !== void 0) {
1308
- if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1309
- const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1310
- if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1311
- for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1312
- if (!safeText3(value2, 1024)) {
1313
- throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1314
- }
1315
- }
1316
- }
1317
- if (google.bookingPageUrl !== void 0) {
1318
- if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1319
- const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1320
- if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1321
- for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1322
- if (value2 !== null && !safeHttpsUrl(value2)) {
1323
- throw new Error(`${path}: calendar.google.bookingPageUrl.${env} must be an HTTPS URL without credentials or fragment`);
1324
- }
1325
- }
1326
- }
1327
- }
1328
1378
  function validateServices(services, path) {
1329
1379
  for (const service of services) {
1330
1380
  const definition = (0, import_apps.appServiceDefinition)(service);
@@ -1338,25 +1388,6 @@ function validateServices(services, path) {
1338
1388
  }
1339
1389
  }
1340
1390
  }
1341
- function assertOnly2(value2, allowed, label) {
1342
- const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1343
- if (extra) throw new Error(`${label}.${extra} is not supported`);
1344
- }
1345
- function isRecord6(value2) {
1346
- return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1347
- }
1348
- function safeText3(value2, max) {
1349
- return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1350
- }
1351
- function safeHttpsUrl(value2) {
1352
- if (typeof value2 !== "string" || value2.length > 2048) return false;
1353
- try {
1354
- const url = new URL(value2);
1355
- return url.protocol === "https:" && !url.username && !url.password && !url.hash;
1356
- } catch {
1357
- return false;
1358
- }
1359
- }
1360
1391
  function validId2(value2) {
1361
1392
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1362
1393
  }
@@ -1371,7 +1402,7 @@ async function loadConfigModule(path) {
1371
1402
  function trimSlash(value2) {
1372
1403
  return value2.replace(/\/+$/, "");
1373
1404
  }
1374
- function unique2(values) {
1405
+ function unique3(values) {
1375
1406
  return [...new Set(values.filter(Boolean))];
1376
1407
  }
1377
1408
 
@@ -4196,6 +4227,67 @@ function isRecord7(value2) {
4196
4227
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
4197
4228
  }
4198
4229
 
4230
+ // src/secret-contract.ts
4231
+ var APP_SOURCE = "app";
4232
+ function resolveSecretContract(cfg) {
4233
+ const byName = /* @__PURE__ */ new Map();
4234
+ const declarations = [
4235
+ ...(cfg.secrets ?? []).map((secret) => ({ source: APP_SOURCE, secret })),
4236
+ ...(cfg.integrations ?? []).flatMap(
4237
+ (integration) => (integration.secrets ?? []).map((secret) => ({ source: integration.id, secret }))
4238
+ )
4239
+ ];
4240
+ for (const { source, secret } of declarations) {
4241
+ const existing = byName.get(secret.name);
4242
+ if (!existing) {
4243
+ byName.set(secret.name, { ...secret, required: secret.required !== false, sources: [source] });
4244
+ continue;
4245
+ }
4246
+ existing.sources.push(source);
4247
+ existing.required = existing.required || secret.required !== false;
4248
+ existing.pattern ??= secret.pattern;
4249
+ }
4250
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
4251
+ }
4252
+ function secretContractWarnings(contract, cfg) {
4253
+ const warnings = [];
4254
+ const declaredPatterns = /* @__PURE__ */ new Map();
4255
+ for (const integration of cfg.integrations ?? []) {
4256
+ for (const secret of integration.secrets ?? []) {
4257
+ if (!secret.pattern) continue;
4258
+ const seen = declaredPatterns.get(secret.name) ?? /* @__PURE__ */ new Map();
4259
+ seen.set(integration.id, secret.pattern);
4260
+ declaredPatterns.set(secret.name, seen);
4261
+ }
4262
+ }
4263
+ for (const secret of cfg.secrets ?? []) {
4264
+ if (!secret.pattern) continue;
4265
+ const seen = declaredPatterns.get(secret.name) ?? /* @__PURE__ */ new Map();
4266
+ seen.set(APP_SOURCE, secret.pattern);
4267
+ declaredPatterns.set(secret.name, seen);
4268
+ }
4269
+ for (const [name, seen] of declaredPatterns) {
4270
+ const distinct = [...new Set(seen.values())];
4271
+ if (distinct.length > 1) {
4272
+ const detail = [...seen].map(([source, pattern]) => `${source} expects "${pattern}"`).join(", ");
4273
+ warnings.push(`secret "${name}" has conflicting patterns \u2014 ${detail}; one of them will reject a valid value`);
4274
+ }
4275
+ }
4276
+ if (contract.length > 0 && !cfg.services.includes("db")) {
4277
+ const names = contract.map((secret) => secret.name).join(", ");
4278
+ warnings.push(`secrets are declared (${names}) but the db service is off \u2014 nothing can read the tenant vault`);
4279
+ }
4280
+ return warnings;
4281
+ }
4282
+ function formatSecretContract(contract) {
4283
+ return contract.map((secret) => {
4284
+ const flags = [secret.required ? "required" : "optional"];
4285
+ if (secret.reserved) flags.push("reserved");
4286
+ if (secret.pattern) flags.push(`${secret.pattern}\u2026`);
4287
+ return ` ${secret.name} (${flags.join(", ")}) \u2014 ${secret.sources.join(", ")}`;
4288
+ });
4289
+ }
4290
+
4199
4291
  // src/doctor.ts
4200
4292
  async function doctor(options) {
4201
4293
  const out = options.stdout ?? console;
@@ -4212,6 +4304,9 @@ async function doctor(options) {
4212
4304
  out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
4213
4305
  out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
4214
4306
  out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4307
+ const contract = resolveSecretContract(cfg);
4308
+ out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
4309
+ for (const line of formatSecretContract(contract)) out.log(line);
4215
4310
  if (cfg.services.includes("calendar")) {
4216
4311
  const calendar = cfg.envs.map((env) => {
4217
4312
  const resolved = calendarServiceConfig(cfg, env);
@@ -4232,6 +4327,7 @@ async function doctor(options) {
4232
4327
  }
4233
4328
  }
4234
4329
  warnings.push(...integrationWarnings(database.integrations, schema, rules));
4330
+ warnings.push(...secretContractWarnings(contract, cfg));
4235
4331
  if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
4236
4332
  warnings.push("ai.mode is byok but ai.provider is not set");
4237
4333
  }
@@ -4558,6 +4654,71 @@ async function resolveVaultWrite(options) {
4558
4654
  return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, env), value: value2, doFetch, out };
4559
4655
  }
4560
4656
 
4657
+ // src/secrets-status.ts
4658
+ var import_apps11 = require("@odla-ai/apps");
4659
+ async function secretsStatus(options) {
4660
+ const out = options.stdout ?? console;
4661
+ const doFetch = options.fetch ?? fetch;
4662
+ const cfg = await loadProjectConfig(options.configPath);
4663
+ if (!cfg.envs.includes(options.env)) {
4664
+ throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
4665
+ }
4666
+ const tenantId = (0, import_apps11.tenantIdFor)(cfg.app.id, options.env);
4667
+ const contract = resolveSecretContract(cfg);
4668
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
4669
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/secrets`, {
4670
+ headers: { authorization: `Bearer ${token}` }
4671
+ });
4672
+ if (!res.ok) {
4673
+ const detail = (await res.text().catch(() => "")).slice(0, 300);
4674
+ throw new Error(`list secrets for ${tenantId} failed (${res.status}): ${detail || "request failed"}`);
4675
+ }
4676
+ const body = await res.json();
4677
+ const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
4678
+ const report4 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
4679
+ if (options.json) out.log(JSON.stringify(report4, null, 2));
4680
+ else printReport(report4, out);
4681
+ return report4;
4682
+ }
4683
+ function buildReport(appId, env, tenant, contract, stored) {
4684
+ const declared = new Set(contract.map((secret) => secret.name));
4685
+ const rows = contract.map((secret) => ({
4686
+ name: secret.name,
4687
+ state: secret.reserved ? "reserved" : stored.has(secret.name) ? "set" : "missing",
4688
+ required: secret.required,
4689
+ sources: secret.sources,
4690
+ description: secret.description
4691
+ }));
4692
+ for (const name of [...stored].sort()) {
4693
+ if (!declared.has(name)) rows.push({ name, state: "undeclared", required: false, sources: [] });
4694
+ }
4695
+ const ok = rows.every((row) => row.state !== "missing" || !row.required);
4696
+ return { app: appId, env, tenant, secrets: rows, ok };
4697
+ }
4698
+ function printReport(report4, out) {
4699
+ out.log(`${report4.app} (${report4.tenant})`);
4700
+ if (report4.secrets.length === 0) {
4701
+ out.log(" no secrets declared and none stored");
4702
+ return;
4703
+ }
4704
+ for (const row of report4.secrets) {
4705
+ const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
4706
+ const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
4707
+ out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
4708
+ }
4709
+ const missing = report4.secrets.filter((row) => row.state === "missing" && row.required);
4710
+ if (missing.length) {
4711
+ out.log("");
4712
+ for (const row of missing) {
4713
+ out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report4.env} --stdin"`);
4714
+ }
4715
+ }
4716
+ if (report4.secrets.some((row) => row.state === "reserved")) {
4717
+ out.log("");
4718
+ out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
4719
+ }
4720
+ }
4721
+
4561
4722
  // src/skill.ts
4562
4723
  var import_node_fs13 = require("fs");
4563
4724
  var import_node_os2 = require("os");
@@ -5027,9 +5188,22 @@ async function secretsCommand(parsed, deps) {
5027
5188
  await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
5028
5189
  return;
5029
5190
  }
5191
+ if (sub === "status") {
5192
+ assertArgs(parsed, ["config", "env", "token", "email", "json"], 2);
5193
+ await secretsStatus({
5194
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
5195
+ env: requiredString(parsed.options.env, "--env"),
5196
+ json: parsed.options.json === true,
5197
+ token: stringOpt(parsed.options.token),
5198
+ email: stringOpt(parsed.options.email),
5199
+ fetch: deps.fetch,
5200
+ stdout: deps.stdout
5201
+ });
5202
+ return;
5203
+ }
5030
5204
  if (sub !== "push") {
5031
5205
  throw new Error(
5032
- `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".`
5206
+ `unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev", "odla-ai secrets status --env dev", "odla-ai secrets set <name> --env dev --stdin", or "odla-ai secrets set-clerk-key --env dev --stdin".`
5033
5207
  );
5034
5208
  }
5035
5209
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
@@ -5553,7 +5727,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
5553
5727
  }
5554
5728
  }
5555
5729
 
5556
- // ../harness/dist/chunk-5FFR7U4L.js
5730
+ // ../harness/dist/chunk-ANNX7VGK.js
5557
5731
  var import_crypto = require("crypto");
5558
5732
  var import_promises5 = require("fs/promises");
5559
5733
  var import_path5 = require("path");
@@ -5613,9 +5787,9 @@ function dependenciesOf(values, influence = "data") {
5613
5787
  result.push({ ref, influence, promptSafetyAtUse: value2.label.promptSafety });
5614
5788
  }
5615
5789
  }
5616
- const unique3 = /* @__PURE__ */ new Map();
5617
- for (const dep of result) unique3.set(`${dep.ref.kind}\0${dep.ref.id}\0${dep.influence}\0${dep.promptSafetyAtUse}`, dep);
5618
- return [...unique3.values()];
5790
+ const unique4 = /* @__PURE__ */ new Map();
5791
+ for (const dep of result) unique4.set(`${dep.ref.kind}\0${dep.ref.id}\0${dep.influence}\0${dep.promptSafetyAtUse}`, dep);
5792
+ return [...unique4.values()];
5619
5793
  }
5620
5794
 
5621
5795
  // ../camel/dist/chunk-4DQ6BIHP.js
@@ -5890,7 +6064,7 @@ function validateSnapshot(snapshot, limits) {
5890
6064
  }
5891
6065
  }
5892
6066
 
5893
- // ../harness/dist/chunk-5FFR7U4L.js
6067
+ // ../harness/dist/chunk-ANNX7VGK.js
5894
6068
  var import_child_process4 = require("child_process");
5895
6069
  var import_promises6 = require("fs/promises");
5896
6070
  var import_path6 = require("path");
@@ -6192,7 +6366,7 @@ function looksLikeDestination(value2) {
6192
6366
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
6193
6367
  }
6194
6368
 
6195
- // ../harness/dist/chunk-5FFR7U4L.js
6369
+ // ../harness/dist/chunk-ANNX7VGK.js
6196
6370
  var import_promises10 = require("fs/promises");
6197
6371
  var import_promises11 = require("fs/promises");
6198
6372
  var import_path10 = require("path");
@@ -6460,7 +6634,7 @@ async function buildCodeGraph(input) {
6460
6634
  return builder.build();
6461
6635
  }
6462
6636
 
6463
- // ../harness/dist/chunk-5FFR7U4L.js
6637
+ // ../harness/dist/chunk-ANNX7VGK.js
6464
6638
  var import_crypto4 = require("crypto");
6465
6639
  async function digestStagedWorkspace(root, limits) {
6466
6640
  const files = [];
@@ -6647,6 +6821,16 @@ function createCodeRuntimeControlClient(options) {
6647
6821
  }
6648
6822
  await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
6649
6823
  },
6824
+ recallMemories: async (sessionId, subjects, limit) => {
6825
+ const response2 = await call2(
6826
+ `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
6827
+ { subjects: [...subjects], limit }
6828
+ );
6829
+ return Array.isArray(response2.memories) ? response2.memories : [];
6830
+ },
6831
+ rememberMemory: async (sessionId, memory) => {
6832
+ await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
6833
+ },
6650
6834
  reportSessionFailure: async (sessionId, message2) => {
6651
6835
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
6652
6836
  await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
@@ -8341,6 +8525,29 @@ function validateOptions(options) {
8341
8525
  throw new TypeError("Code tool broker read-only prefix is invalid");
8342
8526
  }
8343
8527
  }
8528
+ var MAX_MEMORY_BODY = 4e3;
8529
+ function validateMemory(memory) {
8530
+ if (!memory.subject.includes(":")) {
8531
+ throw new TypeError(`memory subject must be a graph node id, got "${memory.subject}"`);
8532
+ }
8533
+ const body = memory.body.trim();
8534
+ if (!body) throw new TypeError("a memory needs a body");
8535
+ if (body.length > MAX_MEMORY_BODY) throw new TypeError("memory body exceeds its bound");
8536
+ if (!memory.authorId.trim()) throw new TypeError("a memory needs an author");
8537
+ }
8538
+ function hazardFromAttempt(input) {
8539
+ const body = [
8540
+ `Attempt ${input.attempt} at "${input.goal.slice(0, 200)}" failed its proof.`,
8541
+ input.feedback.replace(/\s+/g, " ").slice(0, MAX_MEMORY_BODY - 300)
8542
+ ].join(" ");
8543
+ return input.touched.slice(0, 10).map((path) => ({
8544
+ subject: path.includes(":") ? path : `file:${path}`,
8545
+ kind: "hazard",
8546
+ body,
8547
+ evidence: { kind: "gate", ref: input.verificationId },
8548
+ authorId: input.authorId
8549
+ }));
8550
+ }
8344
8551
  async function runGoal(spec, attempt) {
8345
8552
  assertBudget(spec.budget);
8346
8553
  const now = spec.now ?? Date.now;
@@ -8532,6 +8739,9 @@ function pursueRuntimeGoal(input) {
8532
8739
  return { gatePassed: false, feedback: "", tokens: outcome.tokens, error: outcome.error };
8533
8740
  }
8534
8741
  const verdict = await input.gate(attempt);
8742
+ if (!verdict.passed && input.memory) {
8743
+ await rememberFailure(input, attempt, verdict.feedback);
8744
+ }
8535
8745
  return {
8536
8746
  gatePassed: verdict.passed,
8537
8747
  feedback: verdict.feedback,
@@ -8542,6 +8752,25 @@ function pursueRuntimeGoal(input) {
8542
8752
  }
8543
8753
  );
8544
8754
  }
8755
+ async function rememberFailure(input, attempt, feedback) {
8756
+ if (!input.memory || !feedback.trim()) return;
8757
+ try {
8758
+ const touched = await input.touched?.(attempt) ?? [];
8759
+ if (touched.length === 0) return;
8760
+ for (const memory of hazardFromAttempt({
8761
+ goal: input.spec.goal,
8762
+ attempt,
8763
+ feedback,
8764
+ touched,
8765
+ verificationId: `goal-${attempt}`,
8766
+ authorId: input.memory.authorId
8767
+ })) {
8768
+ validateMemory(memory);
8769
+ await input.memory.store.remember(memory);
8770
+ }
8771
+ } catch {
8772
+ }
8773
+ }
8545
8774
  function goalEventLine(event) {
8546
8775
  if (event.type === "attempt_started") return `Goal attempt ${event.attempt} starting.`;
8547
8776
  if (event.type === "attempt_failed") return `Attempt ${event.attempt} did not satisfy the proof.`;
@@ -9806,7 +10035,9 @@ Commands:
9806
10035
  copilot, gemini, or agents (repeatable or comma-separated).
9807
10036
  secrets Push configured db/o11y secrets into the Worker via wrangler
9808
10037
  stdin; set stores a tenant-vault secret and set-clerk-key the
9809
- reserved Clerk secret key, write-only from stdin or an env var.
10038
+ reserved Clerk secret key, write-only from stdin or an env var;
10039
+ status compares the secrets the config declares against the
10040
+ names the environment's vault holds (--json for a report).
9810
10041
  version Print the CLI version.
9811
10042
 
9812
10043
  Safety:
@@ -11789,7 +12020,7 @@ async function read2(url, headers, doFetch) {
11789
12020
  }
11790
12021
 
11791
12022
  // src/provision.ts
11792
- var import_apps12 = require("@odla-ai/apps");
12023
+ var import_apps13 = require("@odla-ai/apps");
11793
12024
  var import_ai5 = require("@odla-ai/ai");
11794
12025
  var import_node_process12 = __toESM(require("process"), 1);
11795
12026
 
@@ -11846,9 +12077,9 @@ async function responseText(res) {
11846
12077
  }
11847
12078
 
11848
12079
  // src/provision-credentials.ts
11849
- var import_apps11 = require("@odla-ai/apps");
12080
+ var import_apps12 = require("@odla-ai/apps");
11850
12081
  async function provisionEnvCredentials(opts) {
11851
- const tenantId = (0, import_apps11.tenantIdFor)(opts.cfg.app.id, opts.env);
12082
+ const tenantId = (0, import_apps12.tenantIdFor)(opts.cfg.app.id, opts.env);
11852
12083
  const prior = opts.credentials?.envs[opts.env];
11853
12084
  let credentials = opts.credentials;
11854
12085
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -12121,7 +12352,7 @@ async function provision(options) {
12121
12352
  optionalProjectCapabilities: ["app.manage"],
12122
12353
  forceReview: options.requestGrant
12123
12354
  });
12124
- const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
12355
+ const apps = (0, import_apps13.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
12125
12356
  const existing = await apps.resolveApp(cfg.app.id);
12126
12357
  if (existing) {
12127
12358
  out.log(`app: ${cfg.app.id} already exists`);
@@ -12133,7 +12364,7 @@ async function provision(options) {
12133
12364
  try {
12134
12365
  await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
12135
12366
  } catch (error) {
12136
- if (error instanceof import_apps12.AppsError && error.status === 403) {
12367
+ if (error instanceof import_apps13.AppsError && error.status === 403) {
12137
12368
  throw new Error(
12138
12369
  `app "${cfg.app.id}" does not exist, and this authenticated agent credential has no owner-reviewed app.manage bootstrap grant for that exact id. Run "odla-ai provision --request-grant --email <odla-account>" to open the review URL and continue; developer ownership alone is not agent authority`,
12139
12370
  { cause: error }
@@ -12146,7 +12377,7 @@ async function provision(options) {
12146
12377
  for (const env of cfg.envs) {
12147
12378
  await assertTenantAdminAccess(doFetch, cfg, env, token);
12148
12379
  }
12149
- const serviceOrder = (0, import_apps12.orderAppServices)(cfg.services);
12380
+ const serviceOrder = (0, import_apps13.orderAppServices)(cfg.services);
12150
12381
  for (const env of cfg.envs) {
12151
12382
  for (const service of serviceOrder) {
12152
12383
  if (service === "ai") {
@@ -12180,7 +12411,7 @@ async function provision(options) {
12180
12411
  }
12181
12412
  let devVarsCredentials = credentials;
12182
12413
  for (const env of cfg.envs) {
12183
- const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
12414
+ const tenantId = (0, import_apps13.tenantIdFor)(cfg.app.id, env);
12184
12415
  let dbKey;
12185
12416
  if (options.pushSecrets) {
12186
12417
  const delivered = await deliverRuntimeCredentials(cfg, {
@@ -12382,7 +12613,7 @@ var COMMAND_SURFACE = {
12382
12613
  rm: {},
12383
12614
  lint: {}
12384
12615
  },
12385
- secrets: { push: {}, set: {}, "set-clerk-key": {} },
12616
+ secrets: { push: {}, status: {}, set: {}, "set-clerk-key": {} },
12386
12617
  security: {
12387
12618
  plan: {},
12388
12619
  sources: {},