@odla-ai/chapter 0.26.1 → 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,10 +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_]*$/;
1000
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
+ }
1001
1023
  function resolveNetwork(config, crm) {
1002
1024
  const seen = /* @__PURE__ */ new Set();
1003
1025
  const targets = [];
@@ -1049,24 +1071,81 @@ function resolveNetwork(config, crm) {
1049
1071
  `defineChapter.network.targets.${target.id}.binding: must be an uppercase Worker binding identifier`
1050
1072
  );
1051
1073
  }
1074
+ const sharedNotes = target.sharedNotes ?? [];
1075
+ validateSharedNotes({
1076
+ path: `defineChapter.network.targets.${target.id}.sharedNotes`,
1077
+ values: sharedNotes,
1078
+ crm,
1079
+ fields
1080
+ });
1052
1081
  targets.push({
1053
1082
  id: target.id,
1054
1083
  name: target.name?.trim() || target.id,
1055
1084
  url: url.origin,
1056
1085
  ...binding ? { binding } : {},
1057
1086
  secretName,
1058
- ...fields ? { fields } : {}
1087
+ ...fields ? { fields } : {},
1088
+ sharedNotes
1059
1089
  });
1060
1090
  }
1061
- return { targets };
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
1134
+ });
1135
+ readers.push({ id: reader.id, fields: reader.fields, sharedNotes });
1136
+ }
1137
+ return { targets, readers };
1062
1138
  }
1139
+
1140
+ // src/config.ts
1141
+ var SLUG2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1063
1142
  function isResolvedCrm(x) {
1064
1143
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
1065
1144
  }
1066
1145
  function defineChapter(config) {
1067
1146
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1068
1147
  const { id: id2, name } = config;
1069
- if (typeof id2 !== "string" || !SLUG.test(id2)) {
1148
+ if (typeof id2 !== "string" || !SLUG2.test(id2)) {
1070
1149
  throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
1071
1150
  }
1072
1151
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
@@ -1091,7 +1170,7 @@ function defineChapter(config) {
1091
1170
  const copy = resolveChapterCopy(config.copy);
1092
1171
  const network = resolveNetwork(config, crm);
1093
1172
  const formation = resolveLeaderFormation(config.formation, crm);
1094
- const { schema, rules } = chapterDb(mode, auth);
1173
+ const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0);
1095
1174
  const services = config.services ?? ["db", "calendar", "o11y"];
1096
1175
  const account = config.account ?? "none";
1097
1176
  if (account !== "invite" && account !== "create" && account !== "none") {
@@ -1347,6 +1426,11 @@ function createChapterIntegration(chapter, options = {}) {
1347
1426
  // The snapshot route is deliberately private. A credential-free smoke
1348
1427
  // request must prove that the route is mounted and closed.
1349
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
+ }] : [],
1350
1434
  // Formation metadata is public only when the host explicitly enables
1351
1435
  // the bounded intake contract.
1352
1436
  ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []