@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.
@@ -990,6 +990,111 @@ function safeText(value2, max) {
990
990
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
991
991
  }
992
992
 
993
+ // src/calendar-config.ts
994
+ function calendarServiceConfig(cfg, env) {
995
+ if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
996
+ if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
997
+ const google = cfg.calendar?.google;
998
+ if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
999
+ const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1000
+ if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1001
+ const availability = unique(configured.map((id) => id.trim()));
1002
+ return {
1003
+ provider: "google",
1004
+ access: "book",
1005
+ bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
1006
+ availabilityCalendars: availability
1007
+ };
1008
+ }
1009
+ function calendarBookingPageUrl(cfg, env) {
1010
+ const value2 = cfg.calendar?.google.bookingPageUrl?.[env];
1011
+ if (value2 === void 0 || value2 === null) return value2;
1012
+ return new URL(value2).toString();
1013
+ }
1014
+ function validateCalendarConfig(cfg, envs, services, path) {
1015
+ const enabled = services.includes("calendar");
1016
+ if (!cfg.calendar) {
1017
+ if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1018
+ return;
1019
+ }
1020
+ if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1021
+ assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1022
+ if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1023
+ const google = cfg.calendar.google;
1024
+ assertOnly2(
1025
+ google,
1026
+ ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1027
+ `${path}: calendar.google`
1028
+ );
1029
+ const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
1030
+ if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
1031
+ throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1032
+ }
1033
+ const availability = google[availabilityKey];
1034
+ if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1035
+ const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1036
+ if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1037
+ for (const env of envs) {
1038
+ const ids = availability[env];
1039
+ if (!Array.isArray(ids) || ids.length === 0) {
1040
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1041
+ }
1042
+ }
1043
+ for (const [env, ids] of Object.entries(availability)) {
1044
+ if (!Array.isArray(ids) || ids.length === 0) {
1045
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1046
+ }
1047
+ if (ids.length > 10) {
1048
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1049
+ }
1050
+ if (ids.some((id) => !safeText2(id, 1024))) {
1051
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1052
+ }
1053
+ }
1054
+ if (google.bookingCalendar !== void 0) {
1055
+ if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1056
+ const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1057
+ if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1058
+ for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1059
+ if (!safeText2(value2, 1024)) {
1060
+ throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1061
+ }
1062
+ }
1063
+ }
1064
+ if (google.bookingPageUrl !== void 0) {
1065
+ if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1066
+ const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1067
+ if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1068
+ for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1069
+ if (value2 !== null && !safeHttpsUrl(value2)) {
1070
+ throw new Error(`${path}: calendar.google.bookingPageUrl.${env} must be an HTTPS URL without credentials or fragment`);
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ function assertOnly2(value2, allowed, label) {
1076
+ const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1077
+ if (extra) throw new Error(`${label}.${extra} is not supported`);
1078
+ }
1079
+ function isRecord5(value2) {
1080
+ return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1081
+ }
1082
+ function safeText2(value2, max) {
1083
+ return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1084
+ }
1085
+ function safeHttpsUrl(value2) {
1086
+ if (typeof value2 !== "string" || value2.length > 2048) return false;
1087
+ try {
1088
+ const url = new URL(value2);
1089
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
1090
+ } catch {
1091
+ return false;
1092
+ }
1093
+ }
1094
+ function unique(values) {
1095
+ return [...new Set(values.filter(Boolean))];
1096
+ }
1097
+
993
1098
  // src/integration-validation.ts
994
1099
  function validateIntegrations(cfg, path, defaultServices) {
995
1100
  if (cfg.integrations === void 0) return;
@@ -997,36 +1102,61 @@ function validateIntegrations(cfg, path, defaultServices) {
997
1102
  const ids = /* @__PURE__ */ new Set();
998
1103
  for (const [index, integration] of cfg.integrations.entries()) {
999
1104
  const at = `${path}: integrations[${index}]`;
1000
- if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
1105
+ if (!isRecord6(integration)) throw new Error(`${at} must be an object`);
1001
1106
  if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
1002
1107
  if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
1003
1108
  ids.add(integration.id);
1004
- if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
1005
- if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1006
- if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
1109
+ if (!safeText3(integration.title, 200)) throw new Error(`${at}.title is required`);
1110
+ if (!safeText3(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1111
+ if (integration.schema !== void 0 && (!isRecord6(integration.schema) || !isRecord6(integration.schema.entities))) {
1007
1112
  throw new Error(`${at}.schema must contain an entities object`);
1008
1113
  }
1009
- if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
1114
+ if (integration.rules !== void 0 && !isRecord6(integration.rules)) throw new Error(`${at}.rules must be an object`);
1010
1115
  validateSeeds(integration, at);
1011
1116
  validateProbes(integration, at);
1117
+ validateSecrets(integration.secrets, at);
1012
1118
  }
1013
1119
  const needsDb = cfg.integrations.some((integration) => integration.schema || integration.rules || integration.seeds?.length);
1014
- const services = unique(cfg.services?.length ? cfg.services : defaultServices);
1120
+ const services = unique2(cfg.services?.length ? cfg.services : defaultServices);
1015
1121
  if (needsDb && !services.includes("db")) throw new Error(`${path}: schema/rules/seed integrations require the db service`);
1016
1122
  }
1123
+ var SECRET_NAME = /^\$?[a-z][a-z0-9_]*$/;
1124
+ function validateSecrets(value2, at) {
1125
+ if (value2 === void 0) return;
1126
+ if (!Array.isArray(value2)) throw new Error(`${at}.secrets must be an array`);
1127
+ const names = /* @__PURE__ */ new Set();
1128
+ for (const [index, secret] of value2.entries()) {
1129
+ const sat = `${at}.secrets[${index}]`;
1130
+ if (!isRecord6(secret)) throw new Error(`${sat} must be an object`);
1131
+ if (typeof secret.name !== "string" || !SECRET_NAME.test(secret.name) || secret.name.length > 64) {
1132
+ throw new Error(`${sat}.name must be lowercase snake_case (optionally "$"-prefixed when reserved), e.g. "clerk_webhook_secret"`);
1133
+ }
1134
+ const dollar = secret.name.startsWith("$");
1135
+ if (dollar !== (secret.reserved === true)) {
1136
+ throw new Error(
1137
+ dollar ? `${sat}.name is "$"-prefixed, so it must also set reserved: true` : `${sat} sets reserved: true, so its name must be "$"-prefixed`
1138
+ );
1139
+ }
1140
+ if (!safeText3(secret.description, 500)) throw new Error(`${sat}.description is required \u2014 it is what doctor and the docs show`);
1141
+ if (secret.pattern !== void 0 && !safeText3(secret.pattern, 64)) throw new Error(`${sat}.pattern must be a non-empty prefix string`);
1142
+ if (secret.required !== void 0 && typeof secret.required !== "boolean") throw new Error(`${sat}.required must be a boolean`);
1143
+ if (names.has(secret.name)) throw new Error(`${at} declares secret "${secret.name}" twice`);
1144
+ names.add(secret.name);
1145
+ }
1146
+ }
1017
1147
  function validateSeeds(integration, at) {
1018
1148
  if (integration.seeds === void 0) return;
1019
1149
  if (!Array.isArray(integration.seeds)) throw new Error(`${at}.seeds must be an array`);
1020
1150
  const ids = /* @__PURE__ */ new Set();
1021
1151
  for (const [index, seed] of integration.seeds.entries()) {
1022
1152
  const sat = `${at}.seeds[${index}]`;
1023
- if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1153
+ if (!isRecord6(seed) || !safeText3(seed.id, 200) || !safeText3(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
1024
1154
  if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
1025
1155
  ids.add(seed.id);
1026
- if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
1156
+ if (!isRecord6(seed.key) || !safeText3(seed.key.attr, 200) || !safeText3(seed.key.value, 2048)) {
1027
1157
  throw new Error(`${sat}.key requires string attr and value`);
1028
1158
  }
1029
- if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1159
+ if (!isRecord6(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
1030
1160
  if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
1031
1161
  throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
1032
1162
  }
@@ -1037,16 +1167,16 @@ function validateProbes(integration, at) {
1037
1167
  if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
1038
1168
  for (const [index, probe] of integration.probes.entries()) {
1039
1169
  const pat = `${at}.probes[${index}]`;
1040
- if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1170
+ if (!isRecord6(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
1041
1171
  if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
1042
1172
  throw new Error(`${pat}.expectedStatus must be an HTTP status`);
1043
1173
  }
1044
1174
  }
1045
1175
  }
1046
- function isRecord5(value2) {
1176
+ function isRecord6(value2) {
1047
1177
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1048
1178
  }
1049
- function safeText2(value2, max) {
1179
+ function safeText3(value2, max) {
1050
1180
  return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1051
1181
  }
1052
1182
  function safeProbePath(value2) {
@@ -1055,7 +1185,7 @@ function safeProbePath(value2) {
1055
1185
  function validId(value2) {
1056
1186
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1057
1187
  }
1058
- function unique(values) {
1188
+ function unique2(values) {
1059
1189
  return [...new Set(values.filter(Boolean))];
1060
1190
  }
1061
1191
 
@@ -1075,10 +1205,10 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1075
1205
  validateRawConfig(raw, resolved);
1076
1206
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1077
1207
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
1078
- const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1079
- const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1208
+ const envs = unique3(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1209
+ const services = unique3(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1080
1210
  validateServices(services, resolved);
1081
- validateCalendarConfig(raw, unique2([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1211
+ validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1082
1212
  const local = {
1083
1213
  tokenFile: resolve2(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1084
1214
  credentialsFile: resolve2(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
@@ -1126,26 +1256,6 @@ function buildPlan(cfg) {
1126
1256
  aiProvider: cfg.ai?.provider
1127
1257
  };
1128
1258
  }
1129
- function calendarServiceConfig(cfg, env) {
1130
- if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1131
- if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
1132
- const google = cfg.calendar?.google;
1133
- if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
1134
- const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1135
- if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1136
- const availability = unique2(configured.map((id) => id.trim()));
1137
- return {
1138
- provider: "google",
1139
- access: "book",
1140
- bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
1141
- availabilityCalendars: availability
1142
- };
1143
- }
1144
- function calendarBookingPageUrl(cfg, env) {
1145
- const value2 = cfg.calendar?.google.bookingPageUrl?.[env];
1146
- if (value2 === void 0 || value2 === null) return value2;
1147
- return new URL(value2).toString();
1148
- }
1149
1259
  function rulesFromSchema(schema) {
1150
1260
  const entities = serializedEntities(schema);
1151
1261
  return Object.fromEntries(
@@ -1177,69 +1287,9 @@ function validateRawConfig(raw, path) {
1177
1287
  throw new Error(`${path}: services must be an array of non-empty names`);
1178
1288
  }
1179
1289
  validateAiConfig(cfg, path);
1290
+ validateSecrets(cfg.secrets, `${path}: config`);
1180
1291
  validateIntegrations(cfg, path, DEFAULT_SERVICES);
1181
1292
  }
1182
- function validateCalendarConfig(cfg, envs, services, path) {
1183
- const enabled = services.includes("calendar");
1184
- if (!cfg.calendar) {
1185
- if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
1186
- return;
1187
- }
1188
- if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1189
- assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
1190
- if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
1191
- const google = cfg.calendar.google;
1192
- assertOnly2(
1193
- google,
1194
- ["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
1195
- `${path}: calendar.google`
1196
- );
1197
- const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
1198
- if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
1199
- throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
1200
- }
1201
- const availability = google[availabilityKey];
1202
- if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1203
- const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1204
- if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1205
- for (const env of envs) {
1206
- const ids = availability[env];
1207
- if (!Array.isArray(ids) || ids.length === 0) {
1208
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1209
- }
1210
- }
1211
- for (const [env, ids] of Object.entries(availability)) {
1212
- if (!Array.isArray(ids) || ids.length === 0) {
1213
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1214
- }
1215
- if (ids.length > 10) {
1216
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1217
- }
1218
- if (ids.some((id) => !safeText3(id, 1024))) {
1219
- throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
1220
- }
1221
- }
1222
- if (google.bookingCalendar !== void 0) {
1223
- if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1224
- const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1225
- if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1226
- for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1227
- if (!safeText3(value2, 1024)) {
1228
- throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
1229
- }
1230
- }
1231
- }
1232
- if (google.bookingPageUrl !== void 0) {
1233
- if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1234
- const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1235
- if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1236
- for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1237
- if (value2 !== null && !safeHttpsUrl(value2)) {
1238
- throw new Error(`${path}: calendar.google.bookingPageUrl.${env} must be an HTTPS URL without credentials or fragment`);
1239
- }
1240
- }
1241
- }
1242
- }
1243
1293
  function validateServices(services, path) {
1244
1294
  for (const service of services) {
1245
1295
  const definition = appServiceDefinition(service);
@@ -1253,25 +1303,6 @@ function validateServices(services, path) {
1253
1303
  }
1254
1304
  }
1255
1305
  }
1256
- function assertOnly2(value2, allowed, label) {
1257
- const extra = Object.keys(value2).find((key) => !allowed.includes(key));
1258
- if (extra) throw new Error(`${label}.${extra} is not supported`);
1259
- }
1260
- function isRecord6(value2) {
1261
- return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1262
- }
1263
- function safeText3(value2, max) {
1264
- return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
1265
- }
1266
- function safeHttpsUrl(value2) {
1267
- if (typeof value2 !== "string" || value2.length > 2048) return false;
1268
- try {
1269
- const url = new URL(value2);
1270
- return url.protocol === "https:" && !url.username && !url.password && !url.hash;
1271
- } catch {
1272
- return false;
1273
- }
1274
- }
1275
1306
  function validId2(value2) {
1276
1307
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1277
1308
  }
@@ -1286,7 +1317,7 @@ async function loadConfigModule(path) {
1286
1317
  function trimSlash(value2) {
1287
1318
  return value2.replace(/\/+$/, "");
1288
1319
  }
1289
- function unique2(values) {
1320
+ function unique3(values) {
1290
1321
  return [...new Set(values.filter(Boolean))];
1291
1322
  }
1292
1323
 
@@ -4068,6 +4099,67 @@ function isRecord7(value2) {
4068
4099
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
4069
4100
  }
4070
4101
 
4102
+ // src/secret-contract.ts
4103
+ var APP_SOURCE = "app";
4104
+ function resolveSecretContract(cfg) {
4105
+ const byName = /* @__PURE__ */ new Map();
4106
+ const declarations = [
4107
+ ...(cfg.secrets ?? []).map((secret) => ({ source: APP_SOURCE, secret })),
4108
+ ...(cfg.integrations ?? []).flatMap(
4109
+ (integration) => (integration.secrets ?? []).map((secret) => ({ source: integration.id, secret }))
4110
+ )
4111
+ ];
4112
+ for (const { source, secret } of declarations) {
4113
+ const existing = byName.get(secret.name);
4114
+ if (!existing) {
4115
+ byName.set(secret.name, { ...secret, required: secret.required !== false, sources: [source] });
4116
+ continue;
4117
+ }
4118
+ existing.sources.push(source);
4119
+ existing.required = existing.required || secret.required !== false;
4120
+ existing.pattern ??= secret.pattern;
4121
+ }
4122
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
4123
+ }
4124
+ function secretContractWarnings(contract, cfg) {
4125
+ const warnings = [];
4126
+ const declaredPatterns = /* @__PURE__ */ new Map();
4127
+ for (const integration of cfg.integrations ?? []) {
4128
+ for (const secret of integration.secrets ?? []) {
4129
+ if (!secret.pattern) continue;
4130
+ const seen = declaredPatterns.get(secret.name) ?? /* @__PURE__ */ new Map();
4131
+ seen.set(integration.id, secret.pattern);
4132
+ declaredPatterns.set(secret.name, seen);
4133
+ }
4134
+ }
4135
+ for (const secret of cfg.secrets ?? []) {
4136
+ if (!secret.pattern) continue;
4137
+ const seen = declaredPatterns.get(secret.name) ?? /* @__PURE__ */ new Map();
4138
+ seen.set(APP_SOURCE, secret.pattern);
4139
+ declaredPatterns.set(secret.name, seen);
4140
+ }
4141
+ for (const [name, seen] of declaredPatterns) {
4142
+ const distinct = [...new Set(seen.values())];
4143
+ if (distinct.length > 1) {
4144
+ const detail = [...seen].map(([source, pattern]) => `${source} expects "${pattern}"`).join(", ");
4145
+ warnings.push(`secret "${name}" has conflicting patterns \u2014 ${detail}; one of them will reject a valid value`);
4146
+ }
4147
+ }
4148
+ if (contract.length > 0 && !cfg.services.includes("db")) {
4149
+ const names = contract.map((secret) => secret.name).join(", ");
4150
+ warnings.push(`secrets are declared (${names}) but the db service is off \u2014 nothing can read the tenant vault`);
4151
+ }
4152
+ return warnings;
4153
+ }
4154
+ function formatSecretContract(contract) {
4155
+ return contract.map((secret) => {
4156
+ const flags = [secret.required ? "required" : "optional"];
4157
+ if (secret.reserved) flags.push("reserved");
4158
+ if (secret.pattern) flags.push(`${secret.pattern}\u2026`);
4159
+ return ` ${secret.name} (${flags.join(", ")}) \u2014 ${secret.sources.join(", ")}`;
4160
+ });
4161
+ }
4162
+
4071
4163
  // src/doctor.ts
4072
4164
  async function doctor(options) {
4073
4165
  const out = options.stdout ?? console;
@@ -4084,6 +4176,9 @@ async function doctor(options) {
4084
4176
  out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
4085
4177
  out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
4086
4178
  out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4179
+ const contract = resolveSecretContract(cfg);
4180
+ out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
4181
+ for (const line of formatSecretContract(contract)) out.log(line);
4087
4182
  if (cfg.services.includes("calendar")) {
4088
4183
  const calendar = cfg.envs.map((env) => {
4089
4184
  const resolved = calendarServiceConfig(cfg, env);
@@ -4104,6 +4199,7 @@ async function doctor(options) {
4104
4199
  }
4105
4200
  }
4106
4201
  warnings.push(...integrationWarnings(database.integrations, schema, rules));
4202
+ warnings.push(...secretContractWarnings(contract, cfg));
4107
4203
  if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
4108
4204
  warnings.push("ai.mode is byok but ai.provider is not set");
4109
4205
  }
@@ -4430,6 +4526,71 @@ async function resolveVaultWrite(options) {
4430
4526
  return { cfg, tenantId: tenantIdFor3(cfg.app.id, env), value: value2, doFetch, out };
4431
4527
  }
4432
4528
 
4529
+ // src/secrets-status.ts
4530
+ import { tenantIdFor as tenantIdFor4 } from "@odla-ai/apps";
4531
+ async function secretsStatus(options) {
4532
+ const out = options.stdout ?? console;
4533
+ const doFetch = options.fetch ?? fetch;
4534
+ const cfg = await loadProjectConfig(options.configPath);
4535
+ if (!cfg.envs.includes(options.env)) {
4536
+ throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
4537
+ }
4538
+ const tenantId = tenantIdFor4(cfg.app.id, options.env);
4539
+ const contract = resolveSecretContract(cfg);
4540
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
4541
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/secrets`, {
4542
+ headers: { authorization: `Bearer ${token}` }
4543
+ });
4544
+ if (!res.ok) {
4545
+ const detail = (await res.text().catch(() => "")).slice(0, 300);
4546
+ throw new Error(`list secrets for ${tenantId} failed (${res.status}): ${detail || "request failed"}`);
4547
+ }
4548
+ const body = await res.json();
4549
+ const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
4550
+ const report4 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
4551
+ if (options.json) out.log(JSON.stringify(report4, null, 2));
4552
+ else printReport(report4, out);
4553
+ return report4;
4554
+ }
4555
+ function buildReport(appId, env, tenant, contract, stored) {
4556
+ const declared = new Set(contract.map((secret) => secret.name));
4557
+ const rows = contract.map((secret) => ({
4558
+ name: secret.name,
4559
+ state: secret.reserved ? "reserved" : stored.has(secret.name) ? "set" : "missing",
4560
+ required: secret.required,
4561
+ sources: secret.sources,
4562
+ description: secret.description
4563
+ }));
4564
+ for (const name of [...stored].sort()) {
4565
+ if (!declared.has(name)) rows.push({ name, state: "undeclared", required: false, sources: [] });
4566
+ }
4567
+ const ok = rows.every((row) => row.state !== "missing" || !row.required);
4568
+ return { app: appId, env, tenant, secrets: rows, ok };
4569
+ }
4570
+ function printReport(report4, out) {
4571
+ out.log(`${report4.app} (${report4.tenant})`);
4572
+ if (report4.secrets.length === 0) {
4573
+ out.log(" no secrets declared and none stored");
4574
+ return;
4575
+ }
4576
+ for (const row of report4.secrets) {
4577
+ const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
4578
+ const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
4579
+ out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
4580
+ }
4581
+ const missing = report4.secrets.filter((row) => row.state === "missing" && row.required);
4582
+ if (missing.length) {
4583
+ out.log("");
4584
+ for (const row of missing) {
4585
+ out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report4.env} --stdin"`);
4586
+ }
4587
+ }
4588
+ if (report4.secrets.some((row) => row.state === "reserved")) {
4589
+ out.log("");
4590
+ out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
4591
+ }
4592
+ }
4593
+
4433
4594
  // src/skill.ts
4434
4595
  import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync, writeFileSync as writeFileSync3 } from "fs";
4435
4596
  import { homedir as homedir2 } from "os";
@@ -4899,9 +5060,22 @@ async function secretsCommand(parsed, deps) {
4899
5060
  await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
4900
5061
  return;
4901
5062
  }
5063
+ if (sub === "status") {
5064
+ assertArgs(parsed, ["config", "env", "token", "email", "json"], 2);
5065
+ await secretsStatus({
5066
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
5067
+ env: requiredString(parsed.options.env, "--env"),
5068
+ json: parsed.options.json === true,
5069
+ token: stringOpt(parsed.options.token),
5070
+ email: stringOpt(parsed.options.email),
5071
+ fetch: deps.fetch,
5072
+ stdout: deps.stdout
5073
+ });
5074
+ return;
5075
+ }
4902
5076
  if (sub !== "push") {
4903
5077
  throw new Error(
4904
- `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".`
5078
+ `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".`
4905
5079
  );
4906
5080
  }
4907
5081
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
@@ -5425,7 +5599,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
5425
5599
  }
5426
5600
  }
5427
5601
 
5428
- // ../harness/dist/chunk-5FFR7U4L.js
5602
+ // ../harness/dist/chunk-ANNX7VGK.js
5429
5603
  import { createHash as createHash3 } from "crypto";
5430
5604
  import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
5431
5605
  import { relative as relative4, resolve as resolve10 } from "path";
@@ -5485,9 +5659,9 @@ function dependenciesOf(values, influence = "data") {
5485
5659
  result.push({ ref, influence, promptSafetyAtUse: value2.label.promptSafety });
5486
5660
  }
5487
5661
  }
5488
- const unique3 = /* @__PURE__ */ new Map();
5489
- for (const dep of result) unique3.set(`${dep.ref.kind}\0${dep.ref.id}\0${dep.influence}\0${dep.promptSafetyAtUse}`, dep);
5490
- return [...unique3.values()];
5662
+ const unique4 = /* @__PURE__ */ new Map();
5663
+ for (const dep of result) unique4.set(`${dep.ref.kind}\0${dep.ref.id}\0${dep.influence}\0${dep.promptSafetyAtUse}`, dep);
5664
+ return [...unique4.values()];
5491
5665
  }
5492
5666
 
5493
5667
  // ../camel/dist/chunk-4DQ6BIHP.js
@@ -5762,7 +5936,7 @@ function validateSnapshot(snapshot, limits) {
5762
5936
  }
5763
5937
  }
5764
5938
 
5765
- // ../harness/dist/chunk-5FFR7U4L.js
5939
+ // ../harness/dist/chunk-ANNX7VGK.js
5766
5940
  import { spawn as spawn4 } from "child_process";
5767
5941
  import { lstat as lstat2 } from "fs/promises";
5768
5942
  import { resolve as resolve23, sep as sep3 } from "path";
@@ -6067,7 +6241,7 @@ function looksLikeDestination(value2) {
6067
6241
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
6068
6242
  }
6069
6243
 
6070
- // ../harness/dist/chunk-5FFR7U4L.js
6244
+ // ../harness/dist/chunk-ANNX7VGK.js
6071
6245
  import { readFile as readFile4, stat as stat2 } from "fs/promises";
6072
6246
  import { readFile as readFile3 } from "fs/promises";
6073
6247
  import { join as join33 } from "path";
@@ -6335,7 +6509,7 @@ async function buildCodeGraph(input) {
6335
6509
  return builder.build();
6336
6510
  }
6337
6511
 
6338
- // ../harness/dist/chunk-5FFR7U4L.js
6512
+ // ../harness/dist/chunk-ANNX7VGK.js
6339
6513
  import { createHash as createHash32 } from "crypto";
6340
6514
  async function digestStagedWorkspace(root, limits) {
6341
6515
  const files = [];
@@ -6522,6 +6696,16 @@ function createCodeRuntimeControlClient(options) {
6522
6696
  }
6523
6697
  await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
6524
6698
  },
6699
+ recallMemories: async (sessionId, subjects, limit) => {
6700
+ const response2 = await call2(
6701
+ `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
6702
+ { subjects: [...subjects], limit }
6703
+ );
6704
+ return Array.isArray(response2.memories) ? response2.memories : [];
6705
+ },
6706
+ rememberMemory: async (sessionId, memory) => {
6707
+ await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
6708
+ },
6525
6709
  reportSessionFailure: async (sessionId, message2) => {
6526
6710
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
6527
6711
  await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
@@ -8216,6 +8400,29 @@ function validateOptions(options) {
8216
8400
  throw new TypeError("Code tool broker read-only prefix is invalid");
8217
8401
  }
8218
8402
  }
8403
+ var MAX_MEMORY_BODY = 4e3;
8404
+ function validateMemory(memory) {
8405
+ if (!memory.subject.includes(":")) {
8406
+ throw new TypeError(`memory subject must be a graph node id, got "${memory.subject}"`);
8407
+ }
8408
+ const body = memory.body.trim();
8409
+ if (!body) throw new TypeError("a memory needs a body");
8410
+ if (body.length > MAX_MEMORY_BODY) throw new TypeError("memory body exceeds its bound");
8411
+ if (!memory.authorId.trim()) throw new TypeError("a memory needs an author");
8412
+ }
8413
+ function hazardFromAttempt(input) {
8414
+ const body = [
8415
+ `Attempt ${input.attempt} at "${input.goal.slice(0, 200)}" failed its proof.`,
8416
+ input.feedback.replace(/\s+/g, " ").slice(0, MAX_MEMORY_BODY - 300)
8417
+ ].join(" ");
8418
+ return input.touched.slice(0, 10).map((path) => ({
8419
+ subject: path.includes(":") ? path : `file:${path}`,
8420
+ kind: "hazard",
8421
+ body,
8422
+ evidence: { kind: "gate", ref: input.verificationId },
8423
+ authorId: input.authorId
8424
+ }));
8425
+ }
8219
8426
  async function runGoal(spec, attempt) {
8220
8427
  assertBudget(spec.budget);
8221
8428
  const now = spec.now ?? Date.now;
@@ -8407,6 +8614,9 @@ function pursueRuntimeGoal(input) {
8407
8614
  return { gatePassed: false, feedback: "", tokens: outcome.tokens, error: outcome.error };
8408
8615
  }
8409
8616
  const verdict = await input.gate(attempt);
8617
+ if (!verdict.passed && input.memory) {
8618
+ await rememberFailure(input, attempt, verdict.feedback);
8619
+ }
8410
8620
  return {
8411
8621
  gatePassed: verdict.passed,
8412
8622
  feedback: verdict.feedback,
@@ -8417,6 +8627,25 @@ function pursueRuntimeGoal(input) {
8417
8627
  }
8418
8628
  );
8419
8629
  }
8630
+ async function rememberFailure(input, attempt, feedback) {
8631
+ if (!input.memory || !feedback.trim()) return;
8632
+ try {
8633
+ const touched = await input.touched?.(attempt) ?? [];
8634
+ if (touched.length === 0) return;
8635
+ for (const memory of hazardFromAttempt({
8636
+ goal: input.spec.goal,
8637
+ attempt,
8638
+ feedback,
8639
+ touched,
8640
+ verificationId: `goal-${attempt}`,
8641
+ authorId: input.memory.authorId
8642
+ })) {
8643
+ validateMemory(memory);
8644
+ await input.memory.store.remember(memory);
8645
+ }
8646
+ } catch {
8647
+ }
8648
+ }
8420
8649
  function goalEventLine(event) {
8421
8650
  if (event.type === "attempt_started") return `Goal attempt ${event.attempt} starting.`;
8422
8651
  if (event.type === "attempt_failed") return `Attempt ${event.attempt} did not satisfy the proof.`;
@@ -9681,7 +9910,9 @@ Commands:
9681
9910
  copilot, gemini, or agents (repeatable or comma-separated).
9682
9911
  secrets Push configured db/o11y secrets into the Worker via wrangler
9683
9912
  stdin; set stores a tenant-vault secret and set-clerk-key the
9684
- reserved Clerk secret key, write-only from stdin or an env var.
9913
+ reserved Clerk secret key, write-only from stdin or an env var;
9914
+ status compares the secrets the config declares against the
9915
+ names the environment's vault holds (--json for a report).
9685
9916
  version Print the CLI version.
9686
9917
 
9687
9918
  Safety:
@@ -11664,7 +11895,7 @@ async function read2(url, headers, doFetch) {
11664
11895
  }
11665
11896
 
11666
11897
  // src/provision.ts
11667
- import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor5 } from "@odla-ai/apps";
11898
+ import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor6 } from "@odla-ai/apps";
11668
11899
  import { putSecret as putSecret2 } from "@odla-ai/ai";
11669
11900
  import process13 from "process";
11670
11901
 
@@ -11721,9 +11952,9 @@ async function responseText(res) {
11721
11952
  }
11722
11953
 
11723
11954
  // src/provision-credentials.ts
11724
- import { tenantIdFor as tenantIdFor4 } from "@odla-ai/apps";
11955
+ import { tenantIdFor as tenantIdFor5 } from "@odla-ai/apps";
11725
11956
  async function provisionEnvCredentials(opts) {
11726
- const tenantId = tenantIdFor4(opts.cfg.app.id, opts.env);
11957
+ const tenantId = tenantIdFor5(opts.cfg.app.id, opts.env);
11727
11958
  const prior = opts.credentials?.envs[opts.env];
11728
11959
  let credentials = opts.credentials;
11729
11960
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -12055,7 +12286,7 @@ async function provision(options) {
12055
12286
  }
12056
12287
  let devVarsCredentials = credentials;
12057
12288
  for (const env of cfg.envs) {
12058
- const tenantId = tenantIdFor5(cfg.app.id, env);
12289
+ const tenantId = tenantIdFor6(cfg.app.id, env);
12059
12290
  let dbKey;
12060
12291
  if (options.pushSecrets) {
12061
12292
  const delivered = await deliverRuntimeCredentials(cfg, {
@@ -12257,7 +12488,7 @@ var COMMAND_SURFACE = {
12257
12488
  rm: {},
12258
12489
  lint: {}
12259
12490
  },
12260
- secrets: { push: {}, set: {}, "set-clerk-key": {} },
12491
+ secrets: { push: {}, status: {}, set: {}, "set-clerk-key": {} },
12261
12492
  security: {
12262
12493
  plan: {},
12263
12494
  sources: {},
@@ -14032,9 +14263,9 @@ export {
14032
14263
  getScopedPlatformToken,
14033
14264
  SYSTEM_AI_PURPOSES,
14034
14265
  adminAi,
14035
- GOOGLE_CALENDAR_EVENTS_SCOPE,
14036
14266
  calendarServiceConfig,
14037
14267
  calendarBookingPageUrl,
14268
+ GOOGLE_CALENDAR_EVENTS_SCOPE,
14038
14269
  calendarStatus,
14039
14270
  calendarCalendars,
14040
14271
  calendarConnect,
@@ -14084,4 +14315,4 @@ export {
14084
14315
  isTerminalHostedSecurityStatus,
14085
14316
  runCli
14086
14317
  };
14087
- //# sourceMappingURL=chunk-L6YTOTWU.js.map
14318
+ //# sourceMappingURL=chunk-YSQORU5J.js.map