@odla-ai/chapter 0.42.1 → 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.
@@ -193,6 +193,9 @@ interface ChapterNetworkTarget {
193
193
  /** Record types on which this leader permits remote lifecycle transitions.
194
194
  * The follower must independently opt in and remains pipeline authority. */
195
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;
196
199
  }
197
200
  /** One signed peer allowed to browse a privacy-bounded projection of this
198
201
  * site's CRM. The follower owns this allowlist; a leader target declaration
@@ -202,7 +205,7 @@ interface ChapterNetworkReader {
202
205
  id: string;
203
206
  /** Per-record-type field allowlist. Omitted types and fields never leave
204
207
  * this site. Record id, name, stage, and timestamps are bounded metadata. */
205
- fields: Record<string, readonly string[]>;
208
+ fields?: Record<string, readonly string[]>;
206
209
  /** Record types on which this reader may append an explicitly shared note.
207
210
  * Omitted by default: browse permission never silently grants write access.
208
211
  * Shared notes land in the follower CRM activity feed. */
@@ -214,6 +217,9 @@ interface ChapterNetworkReader {
214
217
  /** Local CRM record types on which this reader may request a lifecycle
215
218
  * transition. Chapter still enforces the local pipeline and gates. */
216
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;
217
223
  }
218
224
  /** Leader/follower network configuration. A follower needs no code config to
219
225
  * receive pushed records. It must explicitly declare `readers` before any peer
@@ -258,6 +264,7 @@ interface ResolvedNetworkTarget {
258
264
  sharedNotes: readonly string[];
259
265
  editableFields: Record<string, readonly string[]>;
260
266
  stageTransitions: readonly string[];
267
+ admissionGrants?: boolean;
261
268
  }
262
269
  /** A validated follower-owned record projection for one signed reader. */
263
270
  interface ResolvedNetworkReader {
@@ -266,6 +273,7 @@ interface ResolvedNetworkReader {
266
273
  sharedNotes: readonly string[];
267
274
  editableFields: Record<string, readonly string[]>;
268
275
  stageTransitions: readonly string[];
276
+ admissionGrants?: boolean;
269
277
  }
270
278
  /** Fully-resolved leader network configuration. */
271
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)) {
@@ -1591,6 +1640,13 @@ function validateStageTransitions(opts) {
1591
1640
  throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
1592
1641
  }
1593
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
+ }
1594
1650
  function resolveNetwork(config, crm) {
1595
1651
  const seen = /* @__PURE__ */ new Set();
1596
1652
  const targets = [];
@@ -1664,6 +1720,11 @@ function resolveNetwork(config, crm) {
1664
1720
  crm,
1665
1721
  readable: readableFields
1666
1722
  });
1723
+ validateAdmissionCapability(
1724
+ `defineChapter.network.targets.${target.id}.admissionGrants`,
1725
+ target.admissionGrants,
1726
+ crm
1727
+ );
1667
1728
  targets.push({
1668
1729
  id: target.id,
1669
1730
  name: target.name?.trim() || target.id,
@@ -1673,7 +1734,8 @@ function resolveNetwork(config, crm) {
1673
1734
  ...fields ? { fields } : {},
1674
1735
  sharedNotes,
1675
1736
  editableFields,
1676
- stageTransitions
1737
+ stageTransitions,
1738
+ ...target.admissionGrants === true ? { admissionGrants: true } : {}
1677
1739
  });
1678
1740
  }
1679
1741
  const readerIds = /* @__PURE__ */ new Set();
@@ -1686,9 +1748,12 @@ function resolveNetwork(config, crm) {
1686
1748
  throw new Error(`defineChapter.network.readers: duplicate reader "${reader.id}"`);
1687
1749
  }
1688
1750
  readerIds.add(reader.id);
1689
- const entries = Object.entries(reader.fields ?? {});
1690
- if (entries.length === 0) {
1691
- 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
+ );
1692
1757
  }
1693
1758
  for (const [type, names] of entries) {
1694
1759
  if (!FIELD.test(type) || names.length === 0 || names.some((name) => !FIELD.test(name))) {
@@ -1718,23 +1783,35 @@ function resolveNetwork(config, crm) {
1718
1783
  path: `defineChapter.network.readers.${reader.id}.sharedNotes`,
1719
1784
  values: sharedNotes,
1720
1785
  crm,
1721
- fields: reader.fields
1786
+ fields: readerFields
1722
1787
  });
1723
1788
  const editableFields = reader.editableFields ?? {};
1724
1789
  validateEditableFields({
1725
1790
  path: `defineChapter.network.readers.${reader.id}.editableFields`,
1726
1791
  values: editableFields,
1727
1792
  crm,
1728
- readable: reader.fields
1793
+ readable: readerFields
1729
1794
  });
1730
1795
  const stageTransitions = reader.stageTransitions ?? [];
1731
1796
  validateStageTransitions({
1732
1797
  path: `defineChapter.network.readers.${reader.id}.stageTransitions`,
1733
1798
  values: stageTransitions,
1734
1799
  crm,
1735
- readable: reader.fields
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 } : {}
1736
1814
  });
1737
- readers.push({ id: reader.id, fields: reader.fields, sharedNotes, editableFields, stageTransitions });
1738
1815
  }
1739
1816
  return { targets, readers };
1740
1817
  }
@@ -1865,7 +1942,8 @@ function defineChapter(config) {
1865
1942
  application,
1866
1943
  Boolean(signupControl),
1867
1944
  Boolean(memberContent),
1868
- Boolean(builderLibrary)
1945
+ Boolean(builderLibrary),
1946
+ network.targets.some((target) => target.admissionGrants)
1869
1947
  );
1870
1948
  const services = config.services ?? ["db", "calendar", "o11y"];
1871
1949
  const account = config.account ?? "none";