@odla-ai/chapter 0.42.0 → 0.42.2

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.
@@ -186,6 +186,16 @@ interface ChapterNetworkTarget {
186
186
  * this follower. Omitted by default: a target never gains write permission
187
187
  * merely because its records are browsable. */
188
188
  sharedNotes?: readonly string[];
189
+ /** Per-record-type upper bound on profile fields this leader permits its
190
+ * admins to edit remotely. The follower must independently delegate the
191
+ * same field; readable fields never become writable implicitly. */
192
+ editableFields?: Record<string, readonly string[]>;
193
+ /** Record types on which this leader permits remote lifecycle transitions.
194
+ * The follower must independently opt in and remains pipeline authority. */
195
+ stageTransitions?: readonly string[];
196
+ /** Allow package-owned desired admission decisions for existing leader CRM
197
+ * people to be delivered to this target. Omitted means default deny. */
198
+ admissionGrants?: boolean;
189
199
  }
190
200
  /** One signed peer allowed to browse a privacy-bounded projection of this
191
201
  * site's CRM. The follower owns this allowlist; a leader target declaration
@@ -195,11 +205,21 @@ interface ChapterNetworkReader {
195
205
  id: string;
196
206
  /** Per-record-type field allowlist. Omitted types and fields never leave
197
207
  * this site. Record id, name, stage, and timestamps are bounded metadata. */
198
- fields: Record<string, readonly string[]>;
208
+ fields?: Record<string, readonly string[]>;
199
209
  /** Record types on which this reader may append an explicitly shared note.
200
210
  * Omitted by default: browse permission never silently grants write access.
201
211
  * Shared notes land in the follower CRM activity feed. */
202
212
  sharedNotes?: readonly string[];
213
+ /** Exact local CRM fields this reader may edit remotely, by record type.
214
+ * Every field must also be readable so the peer can perform an optimistic
215
+ * edit against the value and revision it actually reviewed. */
216
+ editableFields?: Record<string, readonly string[]>;
217
+ /** Local CRM record types on which this reader may request a lifecycle
218
+ * transition. Chapter still enforces the local pipeline and gates. */
219
+ stageTransitions?: readonly string[];
220
+ /** Accept signed desired admission decisions from this reader. Independent
221
+ * of record browsing; an admission-only reader may omit `fields`. */
222
+ admissionGrants?: boolean;
203
223
  }
204
224
  /** Leader/follower network configuration. A follower needs no code config to
205
225
  * receive pushed records. It must explicitly declare `readers` before any peer
@@ -242,12 +262,18 @@ interface ResolvedNetworkTarget {
242
262
  secretName: string;
243
263
  fields?: Record<string, readonly string[]>;
244
264
  sharedNotes: readonly string[];
265
+ editableFields: Record<string, readonly string[]>;
266
+ stageTransitions: readonly string[];
267
+ admissionGrants?: boolean;
245
268
  }
246
269
  /** A validated follower-owned record projection for one signed reader. */
247
270
  interface ResolvedNetworkReader {
248
271
  id: string;
249
272
  fields: Record<string, readonly string[]>;
250
273
  sharedNotes: readonly string[];
274
+ editableFields: Record<string, readonly string[]>;
275
+ stageTransitions: readonly string[];
276
+ admissionGrants?: boolean;
251
277
  }
252
278
  /** Fully-resolved leader network configuration. */
253
279
  interface ResolvedNetwork {
package/dist/index.cjs CHANGED
@@ -486,6 +486,50 @@ var admissionGrantRevisionsEntity = {
486
486
  receivedAt: attr2("number", { indexed: true })
487
487
  }
488
488
  };
489
+ var leaderAdmissionAssignmentsEntity = {
490
+ attrs: {
491
+ id: id(),
492
+ targetId: attr2("string", { indexed: true }),
493
+ personId: attr2("string", { indexed: true }),
494
+ grantId: attr2("string", { indexed: true }),
495
+ decision: attr2("string", { indexed: true }),
496
+ note: attr2("string", { optional: true }),
497
+ expiresAt: attr2("number", { optional: true, indexed: true }),
498
+ active: attr2("boolean", { indexed: true }),
499
+ lastMutationId: attr2("string", { indexed: true }),
500
+ lastRequestDigest: attr2("string"),
501
+ createdAt: attr2("number", { indexed: true }),
502
+ createdBy: attr2("string", { indexed: true }),
503
+ updatedAt: attr2("number", { indexed: true }),
504
+ updatedBy: attr2("string", { indexed: true })
505
+ }
506
+ };
507
+ var leaderAdmissionDeliveriesEntity = {
508
+ attrs: {
509
+ id: id(),
510
+ targetId: attr2("string", { indexed: true }),
511
+ environment: attr2("string", { indexed: true }),
512
+ revision: attr2("number", { indexed: true }),
513
+ digest: attr2("string", { indexed: true }),
514
+ envelope: attr2("json"),
515
+ status: attr2("string", { indexed: true }),
516
+ attempts: attr2("number", { indexed: true }),
517
+ lastAttemptAt: attr2("number", { optional: true, indexed: true }),
518
+ lastError: attr2("string", { optional: true }),
519
+ acknowledgedAt: attr2("number", { optional: true, indexed: true }),
520
+ updatedAt: attr2("number", { indexed: true })
521
+ }
522
+ };
523
+ var leaderAdmissionMutationsEntity = {
524
+ attrs: {
525
+ id: id(),
526
+ targetId: attr2("string", { indexed: true }),
527
+ personId: attr2("string", { indexed: true }),
528
+ requestDigest: attr2("string", { indexed: true }),
529
+ revision: attr2("number", { indexed: true }),
530
+ createdAt: attr2("number", { indexed: true })
531
+ }
532
+ };
489
533
 
490
534
  // src/schema-gifts.ts
491
535
  var attr3 = (type, flags = {}) => ({
@@ -757,7 +801,7 @@ var emailLog = {
757
801
  sentAt: attr4("number", { indexed: true })
758
802
  }
759
803
  };
760
- function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false) {
804
+ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false) {
761
805
  const entities = {};
762
806
  if (mode === "chapter") {
763
807
  entities.applications = applicationsFor(application);
@@ -785,6 +829,11 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
785
829
  entities.builderLibraryControls = builderLibraryControls;
786
830
  entities.builderLibraryLocal = builderLibraryLocal;
787
831
  }
832
+ if (includeLeaderAdmissions) {
833
+ entities.leaderAdmissionAssignments = leaderAdmissionAssignmentsEntity;
834
+ entities.leaderAdmissionDeliveries = leaderAdmissionDeliveriesEntity;
835
+ entities.leaderAdmissionMutations = leaderAdmissionMutationsEntity;
836
+ }
788
837
  const schema = { entities, links: {} };
789
838
  const rules = {};
790
839
  for (const ns of Object.keys(entities)) {
@@ -1553,6 +1602,12 @@ function formationFields(crm, formation) {
1553
1602
  return resolveFormationFlowFields(crm, formation);
1554
1603
  }
1555
1604
 
1605
+ // src/network-contract.ts
1606
+ var DEFAULT_SHARE_FIELDS = {
1607
+ person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
1608
+ company: ["name", "domain", "industry", "location", "linkedin", "notes"]
1609
+ };
1610
+
1556
1611
  // src/config-network.ts
1557
1612
  var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
1558
1613
  var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
@@ -1564,6 +1619,34 @@ function validateSharedNotes(opts) {
1564
1619
  throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
1565
1620
  }
1566
1621
  }
1622
+ function validateEditableFields(opts) {
1623
+ for (const [type, names] of Object.entries(opts.values)) {
1624
+ const readable = opts.readable?.[type];
1625
+ const def = opts.crm.config.types[type];
1626
+ const duplicate = names.some((name, index) => names.indexOf(name) !== index);
1627
+ const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
1628
+ const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
1629
+ if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
1630
+ throw new Error(
1631
+ `${opts.path}.${type}: must contain unique, readable fields declared on CRM type "${type}"`
1632
+ );
1633
+ }
1634
+ }
1635
+ }
1636
+ function validateStageTransitions(opts) {
1637
+ const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1638
+ const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
1639
+ if (duplicate || invalid) {
1640
+ throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
1641
+ }
1642
+ }
1643
+ function validateAdmissionCapability(path, enabled, crm) {
1644
+ if (!enabled) return;
1645
+ const person = crm.config.types.person;
1646
+ if (!person?.emailField || person.fields[person.emailField]?.type !== "email") {
1647
+ throw new Error(`${path}: requires a CRM person type with an emailField`);
1648
+ }
1649
+ }
1567
1650
  function resolveNetwork(config, crm) {
1568
1651
  const seen = /* @__PURE__ */ new Set();
1569
1652
  const targets = [];
@@ -1622,6 +1705,26 @@ function resolveNetwork(config, crm) {
1622
1705
  crm,
1623
1706
  fields
1624
1707
  });
1708
+ const readableFields = fields ?? DEFAULT_SHARE_FIELDS;
1709
+ const editableFields = target.editableFields ?? {};
1710
+ validateEditableFields({
1711
+ path: `defineChapter.network.targets.${target.id}.editableFields`,
1712
+ values: editableFields,
1713
+ crm,
1714
+ readable: readableFields
1715
+ });
1716
+ const stageTransitions = target.stageTransitions ?? [];
1717
+ validateStageTransitions({
1718
+ path: `defineChapter.network.targets.${target.id}.stageTransitions`,
1719
+ values: stageTransitions,
1720
+ crm,
1721
+ readable: readableFields
1722
+ });
1723
+ validateAdmissionCapability(
1724
+ `defineChapter.network.targets.${target.id}.admissionGrants`,
1725
+ target.admissionGrants,
1726
+ crm
1727
+ );
1625
1728
  targets.push({
1626
1729
  id: target.id,
1627
1730
  name: target.name?.trim() || target.id,
@@ -1629,7 +1732,10 @@ function resolveNetwork(config, crm) {
1629
1732
  ...binding ? { binding } : {},
1630
1733
  secretName,
1631
1734
  ...fields ? { fields } : {},
1632
- sharedNotes
1735
+ sharedNotes,
1736
+ editableFields,
1737
+ stageTransitions,
1738
+ ...target.admissionGrants === true ? { admissionGrants: true } : {}
1633
1739
  });
1634
1740
  }
1635
1741
  const readerIds = /* @__PURE__ */ new Set();
@@ -1642,9 +1748,12 @@ function resolveNetwork(config, crm) {
1642
1748
  throw new Error(`defineChapter.network.readers: duplicate reader "${reader.id}"`);
1643
1749
  }
1644
1750
  readerIds.add(reader.id);
1645
- const entries = Object.entries(reader.fields ?? {});
1646
- if (entries.length === 0) {
1647
- throw new Error(`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required`);
1751
+ const readerFields = reader.fields ?? {};
1752
+ const entries = Object.entries(readerFields);
1753
+ if (entries.length === 0 && !reader.admissionGrants) {
1754
+ throw new Error(
1755
+ `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
1756
+ );
1648
1757
  }
1649
1758
  for (const [type, names] of entries) {
1650
1759
  if (!FIELD.test(type) || names.length === 0 || names.some((name) => !FIELD.test(name))) {
@@ -1674,9 +1783,35 @@ function resolveNetwork(config, crm) {
1674
1783
  path: `defineChapter.network.readers.${reader.id}.sharedNotes`,
1675
1784
  values: sharedNotes,
1676
1785
  crm,
1677
- fields: reader.fields
1786
+ fields: readerFields
1787
+ });
1788
+ const editableFields = reader.editableFields ?? {};
1789
+ validateEditableFields({
1790
+ path: `defineChapter.network.readers.${reader.id}.editableFields`,
1791
+ values: editableFields,
1792
+ crm,
1793
+ readable: readerFields
1794
+ });
1795
+ const stageTransitions = reader.stageTransitions ?? [];
1796
+ validateStageTransitions({
1797
+ path: `defineChapter.network.readers.${reader.id}.stageTransitions`,
1798
+ values: stageTransitions,
1799
+ crm,
1800
+ readable: readerFields
1801
+ });
1802
+ validateAdmissionCapability(
1803
+ `defineChapter.network.readers.${reader.id}.admissionGrants`,
1804
+ reader.admissionGrants,
1805
+ crm
1806
+ );
1807
+ readers.push({
1808
+ id: reader.id,
1809
+ fields: readerFields,
1810
+ sharedNotes,
1811
+ editableFields,
1812
+ stageTransitions,
1813
+ ...reader.admissionGrants === true ? { admissionGrants: true } : {}
1678
1814
  });
1679
- readers.push({ id: reader.id, fields: reader.fields, sharedNotes });
1680
1815
  }
1681
1816
  return { targets, readers };
1682
1817
  }
@@ -1807,7 +1942,8 @@ function defineChapter(config) {
1807
1942
  application,
1808
1943
  Boolean(signupControl),
1809
1944
  Boolean(memberContent),
1810
- Boolean(builderLibrary)
1945
+ Boolean(builderLibrary),
1946
+ network.targets.some((target) => target.admissionGrants)
1811
1947
  );
1812
1948
  const services = config.services ?? ["db", "calendar", "o11y"];
1813
1949
  const account = config.account ?? "none";
@@ -3254,14 +3390,6 @@ function findGiftOffer(catalog, offerId, now) {
3254
3390
 
3255
3391
  // src/network.ts
3256
3392
  var import_crm5 = require("@odla-ai/crm");
3257
-
3258
- // src/network-contract.ts
3259
- var DEFAULT_SHARE_FIELDS = {
3260
- person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
3261
- company: ["name", "domain", "industry", "location", "linkedin", "notes"]
3262
- };
3263
-
3264
- // src/network.ts
3265
3393
  function sharedPersonInput(person) {
3266
3394
  const email = person.email.toLowerCase();
3267
3395
  const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();