@odla-ai/chapter 0.26.0 → 0.27.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
@@ -141,6 +141,19 @@ var superAdmins = {
141
141
  createdAt: attr("number", { indexed: true })
142
142
  }
143
143
  };
144
+ var networkNotes = {
145
+ attrs: {
146
+ id: id(),
147
+ recordKey: attr("string", { indexed: true }),
148
+ targetId: attr("string", { indexed: true }),
149
+ recordType: attr("string", { indexed: true }),
150
+ recordId: attr("string", { indexed: true }),
151
+ body: attr("string"),
152
+ authorId: attr("string", { indexed: true }),
153
+ authorEmail: attr("string", { optional: true }),
154
+ createdAt: attr("number", { indexed: true })
155
+ }
156
+ };
144
157
  var applications = {
145
158
  attrs: {
146
159
  id: id(),
@@ -232,7 +245,7 @@ var emailLog = {
232
245
  sentAt: attr("number", { indexed: true })
233
246
  }
234
247
  };
235
- function chapterDb(mode, auth) {
248
+ function chapterDb(mode, auth, includeNetworkNotes = false) {
236
249
  const entities = {};
237
250
  if (mode === "chapter") {
238
251
  entities.applications = applications;
@@ -242,6 +255,7 @@ function chapterDb(mode, auth) {
242
255
  }
243
256
  if (auth.source === "table") entities.admins = admins;
244
257
  if (auth.superAdmins) entities.superAdmins = superAdmins;
258
+ if (includeNetworkNotes) entities.networkNotes = networkNotes;
245
259
  const schema = { entities, links: {} };
246
260
  const rules = {};
247
261
  for (const ns of Object.keys(entities)) {
@@ -633,6 +647,7 @@ var DEFAULT_ADMIN_COPY = {
633
647
  overview: "Overview",
634
648
  billing: "Billing",
635
649
  people: "People",
650
+ network: "Network",
636
651
  portfolio: "Portfolio",
637
652
  settings: "Settings",
638
653
  calendar: "Calendar",
@@ -994,9 +1009,17 @@ function formationFields(crm, formation) {
994
1009
  });
995
1010
  }
996
1011
 
997
- // src/config.ts
1012
+ // src/config-network.ts
998
1013
  var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
999
1014
  var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
1015
+ var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
1016
+ function validateSharedNotes(opts) {
1017
+ const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1018
+ const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type] || (opts.fields ? !opts.fields[type] : false));
1019
+ if (duplicate || invalid) {
1020
+ throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
1021
+ }
1022
+ }
1000
1023
  function resolveNetwork(config, crm) {
1001
1024
  const seen = /* @__PURE__ */ new Set();
1002
1025
  const targets = [];
@@ -1042,23 +1065,87 @@ function resolveNetwork(config, crm) {
1042
1065
  if (!/^[a-zA-Z][a-zA-Z0-9_-]{1,127}$/.test(secretName)) {
1043
1066
  throw new Error(`defineChapter.network.targets.${target.id}.secretName: must be a vault-key identifier`);
1044
1067
  }
1068
+ const binding = target.binding?.trim();
1069
+ if (binding && !BINDING.test(binding)) {
1070
+ throw new Error(
1071
+ `defineChapter.network.targets.${target.id}.binding: must be an uppercase Worker binding identifier`
1072
+ );
1073
+ }
1074
+ const sharedNotes = target.sharedNotes ?? [];
1075
+ validateSharedNotes({
1076
+ path: `defineChapter.network.targets.${target.id}.sharedNotes`,
1077
+ values: sharedNotes,
1078
+ crm,
1079
+ fields
1080
+ });
1045
1081
  targets.push({
1046
1082
  id: target.id,
1047
1083
  name: target.name?.trim() || target.id,
1048
1084
  url: url.origin,
1085
+ ...binding ? { binding } : {},
1049
1086
  secretName,
1050
- ...fields ? { fields } : {}
1087
+ ...fields ? { fields } : {},
1088
+ sharedNotes
1089
+ });
1090
+ }
1091
+ const readerIds = /* @__PURE__ */ new Set();
1092
+ const readers = [];
1093
+ for (const reader of config.network?.readers ?? []) {
1094
+ if (!SLUG.test(reader.id)) {
1095
+ throw new Error(`defineChapter.network.readers.id: must be a lowercase slug \u2014 got ${JSON.stringify(reader.id)}`);
1096
+ }
1097
+ if (readerIds.has(reader.id)) {
1098
+ throw new Error(`defineChapter.network.readers: duplicate reader "${reader.id}"`);
1099
+ }
1100
+ readerIds.add(reader.id);
1101
+ const entries = Object.entries(reader.fields ?? {});
1102
+ if (entries.length === 0) {
1103
+ throw new Error(`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required`);
1104
+ }
1105
+ for (const [type, names] of entries) {
1106
+ if (!FIELD.test(type) || names.length === 0 || names.some((name) => !FIELD.test(name))) {
1107
+ throw new Error(
1108
+ `defineChapter.network.readers.${reader.id}.fields.${type}: type and field names must be non-empty CRM identifiers`
1109
+ );
1110
+ }
1111
+ const def = crm.config.types[type];
1112
+ if (!def) {
1113
+ throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
1114
+ }
1115
+ const unknown = names.filter((field) => !def.fields[field]);
1116
+ if (unknown.length > 0) {
1117
+ throw new Error(
1118
+ `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the local CRM type`
1119
+ );
1120
+ }
1121
+ const nameField = def.nameField ?? "name";
1122
+ if (!names.includes(nameField)) {
1123
+ throw new Error(
1124
+ `defineChapter.network.readers.${reader.id}.fields.${type}: must include required name field "${nameField}"`
1125
+ );
1126
+ }
1127
+ }
1128
+ const sharedNotes = reader.sharedNotes ?? [];
1129
+ validateSharedNotes({
1130
+ path: `defineChapter.network.readers.${reader.id}.sharedNotes`,
1131
+ values: sharedNotes,
1132
+ crm,
1133
+ fields: reader.fields
1051
1134
  });
1135
+ readers.push({ id: reader.id, fields: reader.fields, sharedNotes });
1052
1136
  }
1053
- return { targets };
1137
+ return { targets, readers };
1054
1138
  }
1139
+
1140
+ // src/config.ts
1141
+ var SLUG2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1055
1142
  function isResolvedCrm(x) {
1056
1143
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
1057
1144
  }
1058
1145
  function defineChapter(config) {
1059
1146
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1060
1147
  const { id: id2, name } = config;
1061
- if (typeof id2 !== "string" || !SLUG.test(id2)) {
1148
+ if (typeof id2 !== "string" || !SLUG2.test(id2)) {
1062
1149
  throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
1063
1150
  }
1064
1151
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
@@ -1083,7 +1170,7 @@ function defineChapter(config) {
1083
1170
  const copy = resolveChapterCopy(config.copy);
1084
1171
  const network = resolveNetwork(config, crm);
1085
1172
  const formation = resolveLeaderFormation(config.formation, crm);
1086
- const { schema, rules } = chapterDb(mode, auth);
1173
+ const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0);
1087
1174
  const services = config.services ?? ["db", "calendar", "o11y"];
1088
1175
  const account = config.account ?? "none";
1089
1176
  if (account !== "invite" && account !== "create" && account !== "none") {
@@ -1339,6 +1426,11 @@ function createChapterIntegration(chapter, options = {}) {
1339
1426
  // The snapshot route is deliberately private. A credential-free smoke
1340
1427
  // request must prove that the route is mounted and closed.
1341
1428
  { path: "/api/network/snapshot", expectedStatus: 401 },
1429
+ ...chapter.network.readers.length > 0 ? [{ path: "/api/network/records?type=person", expectedStatus: 401 }] : [],
1430
+ ...chapter.network.readers.some((reader) => reader.sharedNotes.length > 0) ? [{
1431
+ path: "/api/network/shared-notes?type=person&recordId=probe",
1432
+ expectedStatus: 401
1433
+ }] : [],
1342
1434
  // Formation metadata is public only when the host explicitly enables
1343
1435
  // the bounded intake contract.
1344
1436
  ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []