@odla-ai/chapter 0.44.0 → 0.45.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
@@ -122,6 +122,7 @@ __export(index_exports, {
122
122
  joinConfig: () => joinConfig,
123
123
  joinConfigTiers: () => joinConfigTiers,
124
124
  leaderCrmConfig: () => leaderCrmConfig,
125
+ listStripeObjects: () => listStripeObjects,
125
126
  loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
126
127
  loadOfferableTiers: () => loadOfferableTiers,
127
128
  loadTiers: () => loadTiers,
@@ -136,9 +137,11 @@ __export(index_exports, {
136
137
  memberSystemRollbackPlan: () => memberSystemRollbackPlan,
137
138
  membershipPageDocument: () => membershipPageDocument,
138
139
  membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
140
+ networkCommercialSourceKey: () => networkCommercialSourceKey,
139
141
  networkSourceTag: () => networkSourceTag,
140
142
  normalizeAdmissionEmail: () => normalizeAdmissionEmail,
141
143
  normalizeChapterEmail: () => normalizeChapterEmail,
144
+ normalizeNetworkCommercialStripeEvent: () => normalizeNetworkCommercialStripeEvent,
142
145
  normalizeSharedRecord: () => normalizeSharedRecord,
143
146
  normalizeWebhookEvent: () => normalizeWebhookEvent,
144
147
  offerableTiers: () => offerableTiers,
@@ -151,10 +154,12 @@ __export(index_exports, {
151
154
  projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
152
155
  projectMemberContent: () => projectMemberContent,
153
156
  projectMemberSelf: () => projectMemberSelf,
157
+ projectNetworkCommercialEvents: () => projectNetworkCommercialEvents,
154
158
  projectSharedRecord: () => projectSharedRecord,
155
159
  promoteBuilderLibraryResource: () => promoteBuilderLibraryResource,
156
160
  publicGiftOffers: () => publicGiftOffers,
157
161
  reconcileMeetings: () => reconcileMeetings,
162
+ reconcileStripeSnapshot: () => reconcileStripeSnapshot,
158
163
  refundedPatch: () => refundedPatch,
159
164
  render: () => render,
160
165
  renderSignupControlThemeHead: () => renderSignupControlThemeHead,
@@ -168,6 +173,7 @@ __export(index_exports, {
168
173
  resolveJoinContinuation: () => resolveJoinContinuation,
169
174
  resolveLeaderFormation: () => resolveLeaderFormation,
170
175
  resolveMemberContent: () => resolveMemberContent,
176
+ resolveNetworkCommercialReadModel: () => resolveNetworkCommercialReadModel,
171
177
  resolveNetworkPersonRegistry: () => resolveNetworkPersonRegistry,
172
178
  resolvePipeline: () => resolvePipeline,
173
179
  resolveScheduling: () => resolveScheduling,
@@ -241,7 +247,7 @@ function resolveApplication(a) {
241
247
  throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
242
248
  }
243
249
  }
244
- const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
250
+ const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
245
251
  if (duplicate) {
246
252
  throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
247
253
  }
@@ -320,8 +326,8 @@ async function submitApplication(db, chapter, fields, opts) {
320
326
  if (app.requireDisclaimerAck && !acked) {
321
327
  return { ok: false, error: "disclaimerAck is required" };
322
328
  }
323
- const id5 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
324
- const row = { id: id5, status: chapter.pipeline.initial, createdAt: opts.now };
329
+ const id6 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
330
+ const row = { id: id6, status: chapter.pipeline.initial, createdAt: opts.now };
325
331
  for (const f of [...app.required, ...app.optional]) {
326
332
  if (fieldStates[f]?.visible === false) continue;
327
333
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
@@ -345,19 +351,19 @@ async function submitApplication(db, chapter, fields, opts) {
345
351
  }
346
352
  if (acked) row.disclaimerAckAt = opts.now;
347
353
  const ops = [
348
- { t: "update", ns: "applications", id: id5, attrs: row }
354
+ { t: "update", ns: "applications", id: id6, attrs: row }
349
355
  ];
350
356
  if (opts.admissionGrant?.decision === "waive_interview") {
351
357
  ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
352
358
  consumedAt: opts.now,
353
- consumedApplicationId: id5
359
+ consumedApplicationId: id6
354
360
  } });
355
361
  }
356
362
  const { duplicate } = await db.transact(
357
363
  ops,
358
364
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
359
365
  );
360
- return { ok: true, id: id5, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
366
+ return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
361
367
  }
362
368
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
363
369
  return {
@@ -753,6 +759,64 @@ var networkPersonEntities = {
753
759
  networkPersonEvents: networkPersonEventsEntity
754
760
  };
755
761
 
762
+ // src/schema-network-commercial.ts
763
+ var string2 = (flags = {}) => ({
764
+ type: "string",
765
+ unique: flags.unique ?? false,
766
+ indexed: flags.indexed ?? false,
767
+ optional: flags.optional ?? false
768
+ });
769
+ var number2 = (flags = {}) => ({
770
+ type: "number",
771
+ unique: false,
772
+ indexed: flags.indexed ?? false,
773
+ optional: flags.optional ?? false
774
+ });
775
+ var boolean = (flags = {}) => ({
776
+ type: "boolean",
777
+ unique: false,
778
+ indexed: flags.indexed ?? false,
779
+ optional: false
780
+ });
781
+ var json2 = () => ({ type: "json", unique: false, indexed: false, optional: false });
782
+ var id4 = () => string2({ unique: true, indexed: true });
783
+ var events = { attrs: {
784
+ id: id4(),
785
+ eventId: string2({ unique: true, indexed: true }),
786
+ digest: string2({ indexed: true }),
787
+ eventType: string2({ indexed: true }),
788
+ eventCreated: number2({ indexed: true }),
789
+ livemode: boolean({ indexed: true }),
790
+ fact: json2(),
791
+ receivedAt: number2({ indexed: true })
792
+ } };
793
+ var projection = () => ({ attrs: {
794
+ id: id4(),
795
+ providerId: string2({ indexed: true }),
796
+ sourceKey: string2({ indexed: true }),
797
+ status: string2({ indexed: true }),
798
+ snapshot: json2(),
799
+ eventCreated: number2({ indexed: true }),
800
+ updatedAt: number2({ indexed: true })
801
+ } });
802
+ var exceptions = { attrs: {
803
+ id: id4(),
804
+ eventId: string2({ indexed: true }),
805
+ kind: string2({ indexed: true }),
806
+ sourceKey: string2({ indexed: true, optional: true }),
807
+ status: string2({ indexed: true }),
808
+ detail: json2(),
809
+ updatedAt: number2({ indexed: true })
810
+ } };
811
+ var networkCommercialEntities = {
812
+ networkCommercialEvents: events,
813
+ networkCommercialCustomers: projection(),
814
+ networkCommercialSubscriptions: projection(),
815
+ networkCommercialInvoices: projection(),
816
+ networkCommercialRefunds: projection(),
817
+ networkCommercialExceptions: exceptions
818
+ };
819
+
756
820
  // src/schema.ts
757
821
  function attr5(type, flags = {}) {
758
822
  return {
@@ -762,10 +826,10 @@ function attr5(type, flags = {}) {
762
826
  optional: flags.optional ?? false
763
827
  };
764
828
  }
765
- var id4 = () => attr5("string", { unique: true, indexed: true });
829
+ var id5 = () => attr5("string", { unique: true, indexed: true });
766
830
  var admins = {
767
831
  attrs: {
768
- id: id4(),
832
+ id: id5(),
769
833
  email: attr5("string", { unique: true, indexed: true }),
770
834
  name: attr5("string", { optional: true }),
771
835
  note: attr5("string", { optional: true })
@@ -773,7 +837,7 @@ var admins = {
773
837
  };
774
838
  var superAdmins = {
775
839
  attrs: {
776
- id: id4(),
840
+ id: id5(),
777
841
  email: attr5("string", { unique: true, indexed: true }),
778
842
  note: attr5("string", { optional: true }),
779
843
  createdAt: attr5("number", { indexed: true })
@@ -781,7 +845,7 @@ var superAdmins = {
781
845
  };
782
846
  var networkNotes = {
783
847
  attrs: {
784
- id: id4(),
848
+ id: id5(),
785
849
  recordKey: attr5("string", { indexed: true }),
786
850
  targetId: attr5("string", { indexed: true }),
787
851
  recordType: attr5("string", { indexed: true }),
@@ -794,7 +858,7 @@ var networkNotes = {
794
858
  };
795
859
  var signupControls = {
796
860
  attrs: {
797
- id: id4(),
861
+ id: id5(),
798
862
  sourceId: attr5("string", { indexed: true }),
799
863
  targetId: attr5("string", { indexed: true }),
800
864
  environment: attr5("string", { indexed: true }),
@@ -807,7 +871,7 @@ var signupControls = {
807
871
  };
808
872
  var memberContentControls = {
809
873
  attrs: {
810
- id: id4(),
874
+ id: id5(),
811
875
  sourceId: attr5("string", { indexed: true }),
812
876
  targetId: attr5("string", { indexed: true }),
813
877
  environment: attr5("string", { indexed: true }),
@@ -820,7 +884,7 @@ var memberContentControls = {
820
884
  };
821
885
  var memberContentLocal = {
822
886
  attrs: {
823
- id: id4(),
887
+ id: id5(),
824
888
  chapterId: attr5("string", { indexed: true }),
825
889
  environment: attr5("string", { indexed: true }),
826
890
  revision: attr5("number", { indexed: true }),
@@ -831,7 +895,7 @@ var memberContentLocal = {
831
895
  };
832
896
  var builderLibraryControls = {
833
897
  attrs: {
834
- id: id4(),
898
+ id: id5(),
835
899
  sourceId: attr5("string", { indexed: true }),
836
900
  targetId: attr5("string", { indexed: true }),
837
901
  environment: attr5("string", { indexed: true }),
@@ -844,7 +908,7 @@ var builderLibraryControls = {
844
908
  };
845
909
  var builderLibraryLocal = {
846
910
  attrs: {
847
- id: id4(),
911
+ id: id5(),
848
912
  appId: attr5("string", { indexed: true }),
849
913
  environment: attr5("string", { indexed: true }),
850
914
  revision: attr5("number", { indexed: true }),
@@ -855,7 +919,7 @@ var builderLibraryLocal = {
855
919
  };
856
920
  var groups = {
857
921
  attrs: {
858
- id: id4(),
922
+ id: id5(),
859
923
  name: attr5("string"),
860
924
  standardPriceCents: attr5("number"),
861
925
  foundingDiscountCents: attr5("number"),
@@ -878,7 +942,7 @@ var groups = {
878
942
  };
879
943
  var tiers = {
880
944
  attrs: {
881
- id: id4(),
945
+ id: id5(),
882
946
  groupId: attr5("string", { indexed: true }),
883
947
  name: attr5("string"),
884
948
  priceCents: attr5("number"),
@@ -891,7 +955,7 @@ var tiers = {
891
955
  };
892
956
  var meetings = {
893
957
  attrs: {
894
- id: id4(),
958
+ id: id5(),
895
959
  applicationId: attr5("string", { indexed: true }),
896
960
  groupId: attr5("string", { indexed: true }),
897
961
  startAt: attr5("number", { indexed: true }),
@@ -910,7 +974,7 @@ var meetings = {
910
974
  };
911
975
  var emailLog = {
912
976
  attrs: {
913
- id: id4(),
977
+ id: id5(),
914
978
  groupId: attr5("string", { indexed: true }),
915
979
  applicationId: attr5("string", { indexed: true, optional: true }),
916
980
  to: attr5("string", { indexed: true }),
@@ -925,7 +989,7 @@ var emailLog = {
925
989
  sentAt: attr5("number", { indexed: true })
926
990
  }
927
991
  };
928
- function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false) {
992
+ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false, includeNetworkCommercialReadModel = false) {
929
993
  const entities = {};
930
994
  if (mode === "chapter") {
931
995
  entities.applications = applicationsFor(application);
@@ -962,6 +1026,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
962
1026
  if (includeNetworkPersonRegistry) {
963
1027
  Object.assign(entities, networkPersonEntities);
964
1028
  }
1029
+ if (includeNetworkCommercialReadModel) Object.assign(entities, networkCommercialEntities);
965
1030
  const schema = { entities, links: {} };
966
1031
  const rules = {};
967
1032
  for (const ns of Object.keys(entities)) {
@@ -1133,8 +1198,8 @@ function resolveAuth(mode, auth) {
1133
1198
  return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
1134
1199
  }
1135
1200
  function roleFromClaim(payload, auth) {
1136
- const raw = payload[auth.claim];
1137
- return typeof raw === "string" && auth.ladder.includes(raw) ? raw : auth.ladder[0];
1201
+ const raw2 = payload[auth.claim];
1202
+ return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
1138
1203
  }
1139
1204
  function isAdminRole(role, auth) {
1140
1205
  return role === auth.adminRole;
@@ -1613,9 +1678,9 @@ var APPLICATION_LABELS = {
1613
1678
  phone: "Phone",
1614
1679
  state: "State"
1615
1680
  };
1616
- function humanize(id5) {
1617
- const text4 = id5.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1618
- return text4 ? `${text4[0].toUpperCase()}${text4.slice(1)}` : id5;
1681
+ function humanize(id6) {
1682
+ const text5 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1683
+ return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id6;
1619
1684
  }
1620
1685
  function applicationDefs(chapter) {
1621
1686
  try {
@@ -1624,23 +1689,23 @@ function applicationDefs(chapter) {
1624
1689
  return {};
1625
1690
  }
1626
1691
  }
1627
- function applicationType(id5, def) {
1692
+ function applicationType(id6, def) {
1628
1693
  if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
1629
- return id5 === "email" ? "email" : id5 === "linkedin" ? "url" : "string";
1694
+ return id6 === "email" ? "email" : id6 === "linkedin" ? "url" : "string";
1630
1695
  }
1631
1696
  function applicationFlowFields(chapter, application = chapter.application) {
1632
1697
  const defs = applicationDefs(chapter);
1633
- return [...application.required, ...application.optional].map((id5, order) => {
1634
- const def = defs[id5];
1635
- const conditions = application.conditions[id5];
1636
- const type = applicationType(id5, def);
1698
+ return [...application.required, ...application.optional].map((id6, order2) => {
1699
+ const def = defs[id6];
1700
+ const conditions = application.conditions[id6];
1701
+ const type = applicationType(id6, def);
1637
1702
  return {
1638
- id: id5,
1639
- label: def?.label ?? APPLICATION_LABELS[id5] ?? humanize(id5),
1703
+ id: id6,
1704
+ label: def?.label ?? APPLICATION_LABELS[id6] ?? humanize(id6),
1640
1705
  type,
1641
- required: application.required.includes(id5),
1642
- order,
1643
- maxLength: application.maxLen[id5] ?? application.defaultMaxLen,
1706
+ required: application.required.includes(id6),
1707
+ order: order2,
1708
+ maxLength: application.maxLen[id6] ?? application.defaultMaxLen,
1644
1709
  ...type === "enum" && def?.options ? { options: [...def.options] } : {},
1645
1710
  ...conditions ? { conditions: { ...conditions } } : {},
1646
1711
  ...type === "email" ? { emailIdentity: "literal" } : {}
@@ -1650,17 +1715,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
1650
1715
  function resolveFormationFlowFields(crm, formation) {
1651
1716
  if (!formation.enabled) return [];
1652
1717
  const def = crm.type(formation.type);
1653
- return [...formation.required, ...formation.optional].map((id5, order) => {
1654
- const field = def.fields[id5];
1718
+ return [...formation.required, ...formation.optional].map((id6, order2) => {
1719
+ const field = def.fields[id6];
1655
1720
  return {
1656
- id: id5,
1657
- label: field.label ?? humanize(id5),
1721
+ id: id6,
1722
+ label: field.label ?? humanize(id6),
1658
1723
  type: field.type,
1659
- required: formation.required.includes(id5),
1660
- order,
1661
- maxLength: formation.maxLen[id5] ?? formation.defaultMaxLen,
1724
+ required: formation.required.includes(id6),
1725
+ order: order2,
1726
+ maxLength: formation.maxLen[id6] ?? formation.defaultMaxLen,
1662
1727
  ...field.options ? { options: [...field.options] } : {},
1663
- ...id5 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1728
+ ...id6 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1664
1729
  };
1665
1730
  });
1666
1731
  }
@@ -1700,7 +1765,7 @@ function resolveLeaderFormation(input, crm) {
1700
1765
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1701
1766
  }
1702
1767
  }
1703
- const duplicates = [...required, ...optional].filter((field, index, fields) => fields.indexOf(field) !== index);
1768
+ const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
1704
1769
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1705
1770
  const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
1706
1771
  if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
@@ -1751,7 +1816,7 @@ var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
1751
1816
  var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
1752
1817
  var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
1753
1818
  function validateSharedNotes(opts) {
1754
- const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1819
+ const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
1755
1820
  const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type] || (opts.fields ? !opts.fields[type] : false));
1756
1821
  if (duplicate || invalid) {
1757
1822
  throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
@@ -1761,7 +1826,7 @@ function validateEditableFields(opts) {
1761
1826
  for (const [type, names] of Object.entries(opts.values)) {
1762
1827
  const readable = opts.readable?.[type];
1763
1828
  const def = opts.crm.config.types[type];
1764
- const duplicate = names.some((name, index) => names.indexOf(name) !== index);
1829
+ const duplicate = names.some((name, index2) => names.indexOf(name) !== index2);
1765
1830
  const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
1766
1831
  const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
1767
1832
  if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
@@ -1772,7 +1837,7 @@ function validateEditableFields(opts) {
1772
1837
  }
1773
1838
  }
1774
1839
  function validateStageTransitions(opts) {
1775
- const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1840
+ const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
1776
1841
  const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
1777
1842
  if (duplicate || invalid) {
1778
1843
  throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
@@ -1962,22 +2027,50 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
1962
2027
  if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
1963
2028
  const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
1964
2029
  if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
1965
- if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id5) => !SLUG2.test(id5))) {
2030
+ if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id6) => !SLUG2.test(id6))) {
1966
2031
  throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
1967
2032
  }
1968
- const unknown = chapterIds.filter((id5) => !targetIds.includes(id5));
2033
+ const unknown = chapterIds.filter((id6) => !targetIds.includes(id6));
1969
2034
  if (unknown.length) {
1970
- throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id5) => `"${id5}"`).join(", ")} are not configured network targets`);
2035
+ throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id6) => `"${id6}"`).join(", ")} are not configured network targets`);
1971
2036
  }
1972
2037
  return { mode: "shadow", chapterIds };
1973
2038
  }
1974
2039
 
2040
+ // src/config-network-commercial.ts
2041
+ var SLUG3 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
2042
+ var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2043
+ function networkCommercialSourceKey(source) {
2044
+ return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
2045
+ }
2046
+ function resolveNetworkCommercialReadModel(mode, input, targetIds) {
2047
+ if (!input) return null;
2048
+ if (mode !== "hub") throw new Error("defineChapter.networkCommercialReadModel: only hub mode may own the network commercial projection");
2049
+ if (input.mode !== "shadow") throw new Error('defineChapter.networkCommercialReadModel.mode: must be "shadow"');
2050
+ if (!Array.isArray(input.sources) || input.sources.length === 0) {
2051
+ throw new Error("defineChapter.networkCommercialReadModel.sources: at least one exact source is required");
2052
+ }
2053
+ const sources = input.sources.map((source) => ({ ...source }));
2054
+ for (const source of sources) {
2055
+ if (!SLUG3.test(source.appId) || !SLUG3.test(source.chapterId) || !TOKEN.test(source.environment) || !TOKEN.test(source.dataEnvironment) || !TOKEN.test(source.runtime) || source.stripeMode !== "test" && source.stripeMode !== "live") {
2056
+ throw new Error("defineChapter.networkCommercialReadModel.sources: every source requires valid app, chapter, environment, data-environment, runtime, and Stripe mode values");
2057
+ }
2058
+ if (!targetIds.includes(source.chapterId)) {
2059
+ throw new Error(`defineChapter.networkCommercialReadModel.sources: chapter "${source.chapterId}" is not a configured network target`);
2060
+ }
2061
+ }
2062
+ if (new Set(sources.map(networkCommercialSourceKey)).size !== sources.length) {
2063
+ throw new Error("defineChapter.networkCommercialReadModel.sources: exact source partitions must be distinct");
2064
+ }
2065
+ return { mode: "shadow", sources };
2066
+ }
2067
+
1975
2068
  // src/config-signup-control.ts
1976
- var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2069
+ var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1977
2070
  var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
1978
2071
  function resolveSignupControl(input) {
1979
2072
  if (!input) return null;
1980
- if (!SLUG3.test(input.sourceId)) {
2073
+ if (!SLUG4.test(input.sourceId)) {
1981
2074
  throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
1982
2075
  }
1983
2076
  if (input.stripeMode !== "test" && input.stripeMode !== "live") {
@@ -1991,12 +2084,12 @@ function resolveSignupControl(input) {
1991
2084
  }
1992
2085
 
1993
2086
  // src/config-member-content.ts
1994
- var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2087
+ var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1995
2088
  var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
1996
2089
  function resolveMemberContent(input) {
1997
2090
  if (!input) return null;
1998
2091
  const parent = input.parent;
1999
- if (parent && !SLUG4.test(parent.sourceId)) {
2092
+ if (parent && !SLUG5.test(parent.sourceId)) {
2000
2093
  throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
2001
2094
  }
2002
2095
  const secretName = parent?.secretName?.trim() || "member_content_control_secret";
@@ -2011,22 +2104,22 @@ function resolveMemberContent(input) {
2011
2104
  }
2012
2105
 
2013
2106
  // src/config-builder-library.ts
2014
- var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2107
+ var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2015
2108
  function resolveBuilderLibrary(mode, input, network = { targets: [], readers: [] }) {
2016
2109
  if (!input) return null;
2017
2110
  if (input.sources !== void 0 && !Array.isArray(input.sources)) {
2018
2111
  throw new Error("defineChapter.builderLibrary.sources: must be an array");
2019
2112
  }
2020
2113
  const seen = /* @__PURE__ */ new Set();
2021
- const sources = (input.sources ?? []).map((source, index) => {
2022
- if (!source || typeof source !== "object" || !SLUG5.test(source.sourceId)) {
2023
- throw new Error(`defineChapter.builderLibrary.sources[${index}].sourceId: must be a lowercase app slug`);
2114
+ const sources = (input.sources ?? []).map((source, index2) => {
2115
+ if (!source || typeof source !== "object" || !SLUG6.test(source.sourceId)) {
2116
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}].sourceId: must be a lowercase app slug`);
2024
2117
  }
2025
2118
  if (source.sourceId === "built-not-found" && mode === "hub") {
2026
- throw new Error(`defineChapter.builderLibrary.sources[${index}]: a hub accepts Chapter sources, not itself`);
2119
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}]: a hub accepts Chapter sources, not itself`);
2027
2120
  }
2028
2121
  if (mode === "chapter" && source.sourceId !== "built-not-found") {
2029
- throw new Error(`defineChapter.builderLibrary.sources[${index}]: Chapters may accept only the built-not-found source`);
2122
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}]: Chapters may accept only the built-not-found source`);
2030
2123
  }
2031
2124
  if (seen.has(source.sourceId)) {
2032
2125
  throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
@@ -2034,14 +2127,14 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
2034
2127
  seen.add(source.sourceId);
2035
2128
  if ("secretName" in source) {
2036
2129
  throw new Error(
2037
- `defineChapter.builderLibrary.sources[${index}].secretName: remove this field; Builder Library reuses the Chapter network edge`
2130
+ `defineChapter.builderLibrary.sources[${index2}].secretName: remove this field; Builder Library reuses the Chapter network edge`
2038
2131
  );
2039
2132
  }
2040
2133
  const networkPeer = mode === "hub" ? network.targets.some((target) => target.id === source.sourceId) : network.readers.some((reader) => reader.id === source.sourceId);
2041
2134
  if (!networkPeer) {
2042
2135
  const side = mode === "hub" ? "targets" : "readers";
2043
2136
  throw new Error(
2044
- `defineChapter.builderLibrary.sources[${index}]: "${source.sourceId}" must already be configured in network.${side}`
2137
+ `defineChapter.builderLibrary.sources[${index2}]: "${source.sourceId}" must already be configured in network.${side}`
2045
2138
  );
2046
2139
  }
2047
2140
  return { sourceId: source.sourceId };
@@ -2050,15 +2143,15 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
2050
2143
  }
2051
2144
 
2052
2145
  // src/config.ts
2053
- var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2146
+ var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2054
2147
  function isResolvedCrm(x) {
2055
2148
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
2056
2149
  }
2057
2150
  function defineChapter(config) {
2058
2151
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
2059
- const { id: id5, name } = config;
2060
- if (typeof id5 !== "string" || !SLUG6.test(id5)) {
2061
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id5)}`);
2152
+ const { id: id6, name } = config;
2153
+ if (typeof id6 !== "string" || !SLUG7.test(id6)) {
2154
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id6)}`);
2062
2155
  }
2063
2156
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
2064
2157
  const mode = config.mode ?? "chapter";
@@ -2086,6 +2179,11 @@ function defineChapter(config) {
2086
2179
  config.networkPersonRegistry,
2087
2180
  network.targets.map((target) => target.id)
2088
2181
  );
2182
+ const networkCommercialReadModel = resolveNetworkCommercialReadModel(
2183
+ mode,
2184
+ config.networkCommercialReadModel,
2185
+ network.targets.map((target) => target.id)
2186
+ );
2089
2187
  const signupControl = resolveSignupControl(config.signupControl);
2090
2188
  const memberContent = resolveMemberContent(config.memberContent);
2091
2189
  const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary, network);
@@ -2105,7 +2203,8 @@ function defineChapter(config) {
2105
2203
  Boolean(memberContent),
2106
2204
  Boolean(builderLibrary),
2107
2205
  network.targets.some((target) => target.admissionGrants),
2108
- Boolean(networkPersonRegistry)
2206
+ Boolean(networkPersonRegistry),
2207
+ Boolean(networkCommercialReadModel)
2109
2208
  );
2110
2209
  const services = config.services ?? ["db", "calendar", "o11y"];
2111
2210
  const account = config.account ?? "none";
@@ -2155,7 +2254,7 @@ function defineChapter(config) {
2155
2254
  };
2156
2255
  const chapter = {
2157
2256
  config,
2158
- id: id5,
2257
+ id: id6,
2159
2258
  name,
2160
2259
  mode,
2161
2260
  brand,
@@ -2165,6 +2264,7 @@ function defineChapter(config) {
2165
2264
  memberContent,
2166
2265
  builderLibrary,
2167
2266
  networkPersonRegistry,
2267
+ networkCommercialReadModel,
2168
2268
  formation,
2169
2269
  crm,
2170
2270
  auth,
@@ -2182,6 +2282,304 @@ function defineChapter(config) {
2182
2282
  return chapter;
2183
2283
  }
2184
2284
 
2285
+ // src/network-commercial.ts
2286
+ var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2287
+ var text = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
2288
+ var integer = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
2289
+ var bool = (value) => typeof value === "boolean" ? value : void 0;
2290
+ var providerId = (value, prefix) => {
2291
+ const valueText = text(value);
2292
+ return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
2293
+ };
2294
+ function metadataCandidates(object) {
2295
+ const parent = raw(object.parent);
2296
+ const subscriptionDetails = raw(object.subscription_details);
2297
+ const parentSubscription = raw(parent.subscription_details);
2298
+ return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
2299
+ }
2300
+ function metadataValue(candidates, key) {
2301
+ return candidates.map((candidate) => text(candidate[key])).find(Boolean);
2302
+ }
2303
+ function refId(value, prefix) {
2304
+ if (typeof value === "string") return providerId(value, prefix);
2305
+ return providerId(raw(value).id, prefix);
2306
+ }
2307
+ function subscriptionRef(object) {
2308
+ return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
2309
+ }
2310
+ function firstPrice(object) {
2311
+ const data = raw(object.items).data;
2312
+ const item = Array.isArray(data) ? raw(data[0]) : {};
2313
+ const priceId = refId(item.price, "price_");
2314
+ const quantity = integer(item.quantity);
2315
+ return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
2316
+ }
2317
+ function normalizeNetworkCommercialStripeEvent(input, digest) {
2318
+ const event = raw(input);
2319
+ const eventId = providerId(event.id, "evt_") ?? "";
2320
+ const eventType = text(event.type) ?? "";
2321
+ const object = raw(raw(event.data).object);
2322
+ const objectId = providerId(object.id) ?? "";
2323
+ const candidates = metadataCandidates(object);
2324
+ const identity = {};
2325
+ const identityKeys = {
2326
+ appId: "odlaAppId",
2327
+ chapterId: "chapterId",
2328
+ environment: "odlaEnvironment",
2329
+ dataEnvironment: "odlaDataEnvironment",
2330
+ runtime: "odlaRuntime"
2331
+ };
2332
+ for (const [target, source] of Object.entries(identityKeys)) {
2333
+ const value = metadataValue(candidates, source);
2334
+ if (value) identity[target] = value;
2335
+ }
2336
+ let objectKind = "ignored";
2337
+ if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
2338
+ else if (eventType.startsWith("customer.")) objectKind = "customer";
2339
+ else if (eventType.startsWith("invoice.")) objectKind = "invoice";
2340
+ else if (eventType.startsWith("refund.")) objectKind = "refund";
2341
+ else if (eventType === "charge.refunded") objectKind = "refund";
2342
+ const customerId = refId(object.customer, "cus_");
2343
+ const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
2344
+ const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
2345
+ const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
2346
+ const paymentIntentId = refId(object.payment_intent, "pi_");
2347
+ const price = firstPrice(object);
2348
+ const amountCents = integer(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
2349
+ const currency = text(object.currency)?.toLowerCase();
2350
+ const status = text(object.status);
2351
+ return {
2352
+ eventId,
2353
+ digest,
2354
+ eventType,
2355
+ eventCreated: integer(event.created) ?? 0,
2356
+ livemode: event.livemode === true,
2357
+ objectKind,
2358
+ objectId,
2359
+ identity,
2360
+ ...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
2361
+ ...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
2362
+ ...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
2363
+ ...customerId ? { customerId } : {},
2364
+ ...subscriptionId ? { subscriptionId } : {},
2365
+ ...invoiceId ? { invoiceId } : {},
2366
+ ...chargeId ? { chargeId } : {},
2367
+ ...paymentIntentId ? { paymentIntentId } : {},
2368
+ ...price,
2369
+ ...status ? { status } : {},
2370
+ ...currency ? { currency } : {},
2371
+ ...amountCents !== void 0 ? { amountCents } : {},
2372
+ ...integer(object.amount_paid) !== void 0 ? { amountPaidCents: integer(object.amount_paid) } : {},
2373
+ ...integer(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer(object.amount_remaining) } : {},
2374
+ ...integer(object.current_period_start) !== void 0 ? { currentPeriodStart: integer(object.current_period_start) } : {},
2375
+ ...integer(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer(object.current_period_end) } : {},
2376
+ ...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
2377
+ };
2378
+ }
2379
+ var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2380
+ function directSource(fact, sources) {
2381
+ const mode = fact.livemode ? "live" : "test";
2382
+ const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
2383
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
2384
+ if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2385
+ if (supplied.length > 0 && candidates.length === 0) return "wrong";
2386
+ return null;
2387
+ }
2388
+ var objectRefs = (fact) => [
2389
+ fact.customerId && `customer:${fact.customerId}`,
2390
+ fact.subscriptionId && `subscription:${fact.subscriptionId}`,
2391
+ fact.invoiceId && `invoice:${fact.invoiceId}`,
2392
+ fact.chargeId && `charge:${fact.chargeId}`,
2393
+ fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
2394
+ ].filter((value) => Boolean(value));
2395
+ function ownRef(fact) {
2396
+ if (!fact.objectId) return null;
2397
+ if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
2398
+ if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
2399
+ if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
2400
+ if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
2401
+ return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
2402
+ }
2403
+ var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
2404
+ function projectNetworkCommercialEvents(input, sources) {
2405
+ const facts2 = [...input].sort(order);
2406
+ const exceptions2 = [];
2407
+ const unique = [];
2408
+ const digests = /* @__PURE__ */ new Map();
2409
+ for (const fact of facts2) {
2410
+ const prior = digests.get(fact.eventId);
2411
+ if (prior === void 0) {
2412
+ digests.set(fact.eventId, fact.digest);
2413
+ unique.push(fact);
2414
+ } else if (prior !== fact.digest) exceptions2.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2415
+ }
2416
+ const assignments = /* @__PURE__ */ new Map();
2417
+ const refs = /* @__PURE__ */ new Map();
2418
+ for (const fact of unique) {
2419
+ const direct = directSource(fact, sources);
2420
+ if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
2421
+ }
2422
+ let changed = true;
2423
+ while (changed) {
2424
+ changed = false;
2425
+ for (const fact of unique) {
2426
+ const assigned = assignments.get(fact.eventId);
2427
+ if (assigned) {
2428
+ const key = networkCommercialSourceKey(assigned);
2429
+ for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
2430
+ const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
2431
+ const before = keys2.size;
2432
+ keys2.add(key);
2433
+ refs.set(ref, keys2);
2434
+ if (keys2.size !== before) changed = true;
2435
+ }
2436
+ continue;
2437
+ }
2438
+ if (directSource(fact, sources) === "wrong") continue;
2439
+ const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
2440
+ if (keys.size !== 1) continue;
2441
+ const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
2442
+ if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
2443
+ assignments.set(fact.eventId, candidate);
2444
+ changed = true;
2445
+ }
2446
+ }
2447
+ }
2448
+ const latest = /* @__PURE__ */ new Map();
2449
+ for (const fact of unique) {
2450
+ if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
2451
+ exceptions2.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2452
+ continue;
2453
+ }
2454
+ const source = assignments.get(fact.eventId);
2455
+ if (!source) {
2456
+ const direct = directSource(fact, sources);
2457
+ const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
2458
+ exceptions2.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2459
+ continue;
2460
+ }
2461
+ const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
2462
+ const key = `${fact.objectKind}:${fact.objectId}`;
2463
+ const prior = latest.get(key);
2464
+ if (!prior || order(prior, fact) <= 0) latest.set(key, row);
2465
+ }
2466
+ const rows3 = [...latest.values()];
2467
+ const customers = rows3.filter((row) => row.objectKind === "customer");
2468
+ const subscriptions = rows3.filter((row) => row.objectKind === "subscription");
2469
+ const invoices = rows3.filter((row) => row.objectKind === "invoice");
2470
+ const refunds = rows3.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
2471
+ for (const charge of rows3.filter((row) => row.eventType === "charge.refunded")) {
2472
+ if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
2473
+ exceptions2.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
2474
+ }
2475
+ }
2476
+ const totalsMap = /* @__PURE__ */ new Map();
2477
+ const add = (row, field, amount) => {
2478
+ if (!row.currency) return;
2479
+ const key = `${row.sourceKey}:${row.currency}`;
2480
+ const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2481
+ total[field] += amount;
2482
+ total.netCents = total.paidCents - total.refundedCents;
2483
+ totalsMap.set(key, total);
2484
+ };
2485
+ for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
2486
+ for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
2487
+ return { customers, subscriptions, invoices, refunds, exceptions: exceptions2, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
2488
+ }
2489
+
2490
+ // src/network-commercial-reconcile.ts
2491
+ var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2492
+ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
2493
+ const objects = [];
2494
+ let startingAfter;
2495
+ for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
2496
+ const page = await client.request("GET", path, {
2497
+ ...params,
2498
+ limit: 100,
2499
+ ...startingAfter ? { starting_after: startingAfter } : {}
2500
+ });
2501
+ if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
2502
+ throw new Error("Stripe returned an invalid commercial list response");
2503
+ }
2504
+ const rows3 = page.data.map(asRaw).filter((row) => row !== null);
2505
+ if (rows3.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
2506
+ objects.push(...rows3);
2507
+ if (!page.has_more) return { objects, complete: true };
2508
+ const cursor = rows3.at(-1)?.id;
2509
+ if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
2510
+ startingAfter = cursor;
2511
+ }
2512
+ return { objects, complete: false };
2513
+ }
2514
+ function facts(snapshots) {
2515
+ const groups2 = [
2516
+ ["customer.updated", snapshots.customers.objects],
2517
+ ["customer.subscription.updated", snapshots.subscriptions.objects],
2518
+ ["invoice.updated", snapshots.invoices.objects],
2519
+ ["refund.updated", snapshots.refunds.objects]
2520
+ ];
2521
+ return groups2.flatMap(([eventType, objects]) => objects.map((object) => {
2522
+ const id6 = typeof object.id === "string" ? object.id : "invalid";
2523
+ return normalizeNetworkCommercialStripeEvent({
2524
+ id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id6}`,
2525
+ type: eventType,
2526
+ created: typeof object.created === "number" ? object.created : 0,
2527
+ livemode: object.livemode === true,
2528
+ data: { object }
2529
+ }, `stripe-snapshot:${eventType}:${id6}`);
2530
+ }));
2531
+ }
2532
+ var selected = (row) => ({
2533
+ status: row.status ?? null,
2534
+ currency: row.currency ?? null,
2535
+ amountCents: row.amountCents ?? null,
2536
+ amountPaidCents: row.amountPaidCents ?? null,
2537
+ amountRemainingCents: row.amountRemainingCents ?? null,
2538
+ customerId: row.customerId ?? null,
2539
+ subscriptionId: row.subscriptionId ?? null,
2540
+ invoiceId: row.invoiceId ?? null,
2541
+ priceId: row.priceId ?? null,
2542
+ quantity: row.quantity ?? null,
2543
+ applicationId: row.applicationId ?? null,
2544
+ personId: row.personId ?? null,
2545
+ membershipId: row.membershipId ?? null,
2546
+ currentPeriodStart: row.currentPeriodStart ?? null,
2547
+ currentPeriodEnd: row.currentPeriodEnd ?? null,
2548
+ cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
2549
+ });
2550
+ var index = (projection2) => new Map(
2551
+ [...projection2.customers, ...projection2.subscriptions, ...projection2.invoices, ...projection2.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
2552
+ );
2553
+ function reconcileStripeSnapshot(shadow, snapshots, sources) {
2554
+ const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
2555
+ const shadowRows = index(shadow);
2556
+ const stripeRows = index(stripe);
2557
+ const exceptions2 = [];
2558
+ const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
2559
+ if (!complete) exceptions2.push({ kind: "stripe_snapshot_truncated", detail: {} });
2560
+ for (const [key, row] of shadowRows) {
2561
+ const other = stripeRows.get(key);
2562
+ if (!other) {
2563
+ exceptions2.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
2564
+ } else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
2565
+ exceptions2.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
2566
+ }
2567
+ }
2568
+ for (const [key, row] of stripeRows) {
2569
+ if (!shadowRows.has(key)) exceptions2.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
2570
+ }
2571
+ if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
2572
+ exceptions2.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
2573
+ }
2574
+ return {
2575
+ ok: exceptions2.length === 0,
2576
+ complete,
2577
+ ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
2578
+ exceptions: exceptions2,
2579
+ stripeTotals: stripe.totals
2580
+ };
2581
+ }
2582
+
2185
2583
  // src/leader.ts
2186
2584
  var import_crm3 = require("@odla-ai/crm");
2187
2585
  function leaderCrmConfig(options = {}) {
@@ -2350,14 +2748,14 @@ var import_crm4 = require("@odla-ai/crm");
2350
2748
 
2351
2749
  // src/tiers.ts
2352
2750
  function buildTierSeeds(groupId, configured = [], now = 0) {
2353
- return configured.map((tier, index) => ({
2751
+ return configured.map((tier, index2) => ({
2354
2752
  id: tier.id,
2355
2753
  groupId,
2356
2754
  name: tier.name,
2357
2755
  priceCents: tier.priceCents,
2358
2756
  ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
2359
2757
  ...tier.blurb ? { blurb: tier.blurb } : {},
2360
- sortOrder: tier.sortOrder ?? index,
2758
+ sortOrder: tier.sortOrder ?? index2,
2361
2759
  active: tier.active !== false,
2362
2760
  createdAt: now
2363
2761
  }));
@@ -2430,13 +2828,26 @@ function createChapterIntegration(chapter, options = {}) {
2430
2828
  const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
2431
2829
  const runbooks = isLeader ? {
2432
2830
  role: "leader",
2433
- slugs: ["chapter-leader", "chapter-network", "crm"],
2831
+ slugs: ["chapter-leader", "chapter-network", "crm", ...chapter.networkCommercialReadModel ? ["stripe-network-shadow"] : []],
2434
2832
  guidance: `${chapter.name} is a LEADER hub: it holds the CRM and shares records with its chapters. Read it with \`odla-ai runbook get chapter-leader\`.`
2435
2833
  } : {
2436
2834
  role: "follower",
2437
2835
  slugs: ["chapter-follower", "chapter-network"],
2438
2836
  guidance: `${chapter.name} is a FOLLOWER site: it receives people and records from a leader hub. Read it with \`odla-ai runbook get chapter-follower\`.`
2439
2837
  };
2838
+ const secrets = [];
2839
+ if (chapter.account !== "none") secrets.push({
2840
+ // Released Chapter workers request this effective name. odla-db maps it to
2841
+ // the canonical reserved `$clerk_secret` value written by the CLI.
2842
+ name: "clerk_secret_key",
2843
+ description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
2844
+ required: true
2845
+ });
2846
+ if (chapter.networkCommercialReadModel) secrets.push({
2847
+ name: "network_commercial_stripe_webhook_secret",
2848
+ description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
2849
+ required: true
2850
+ });
2440
2851
  return {
2441
2852
  id: "chapter",
2442
2853
  title: `Chapter: ${chapter.name}`,
@@ -2447,14 +2858,7 @@ function createChapterIntegration(chapter, options = {}) {
2447
2858
  },
2448
2859
  rules: { ...crmDesc.rules, ...chapter.rules },
2449
2860
  seeds,
2450
- secrets: chapter.account === "none" ? [] : [{
2451
- // Released Chapter workers request this effective name. odla-db maps
2452
- // it to the canonical reserved `$clerk_secret` value written by
2453
- // `odla-ai secrets set-clerk-key`.
2454
- name: "clerk_secret_key",
2455
- description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
2456
- required: true
2457
- }],
2861
+ secrets,
2458
2862
  probes: [
2459
2863
  ...crmDesc.probes ?? [],
2460
2864
  // The snapshot route is deliberately private. A credential-free smoke
@@ -2477,7 +2881,11 @@ function createChapterIntegration(chapter, options = {}) {
2477
2881
  { path: "/api/admin/builder-library", expectedStatus: 401 },
2478
2882
  { path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
2479
2883
  ] : [],
2480
- ...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : []
2884
+ ...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : [],
2885
+ ...chapter.networkCommercialReadModel ? [
2886
+ { path: "/api/network-commercial/stripe-webhook", expectedStatus: 405 },
2887
+ { path: "/api/admin/network-commercial-read-model", expectedStatus: 401 }
2888
+ ] : []
2481
2889
  ],
2482
2890
  runbooks
2483
2891
  };
@@ -2485,7 +2893,7 @@ function createChapterIntegration(chapter, options = {}) {
2485
2893
 
2486
2894
  // src/member-content-validation-helpers.ts
2487
2895
  var ID = /^[a-z][a-z0-9-]{0,79}$/;
2488
- var SLUG7 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2896
+ var SLUG8 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2489
2897
  var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2490
2898
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2491
2899
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
@@ -2517,21 +2925,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2517
2925
  return valueText;
2518
2926
  }
2519
2927
  function offerValue(value, path, errors, tiers2, audience) {
2520
- const raw = record(value);
2521
- if (!raw) {
2928
+ const raw2 = record(value);
2929
+ if (!raw2) {
2522
2930
  errors.push(`${path} must be an object`);
2523
2931
  return void 0;
2524
2932
  }
2525
- unknownKeys(raw, OFFER_KEYS, path, errors);
2526
- const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2933
+ unknownKeys(raw2, OFFER_KEYS, path, errors);
2934
+ const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2527
2935
  if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2528
2936
  if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2529
2937
  if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2530
2938
  errors.push(`${path}.tierId must grant access to this page`);
2531
2939
  }
2532
- const label = textValue(raw.label, `${path}.label`, errors);
2533
- const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
2534
- const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2940
+ const label = textValue(raw2.label, `${path}.label`, errors);
2941
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
2942
+ const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
2535
2943
  return {
2536
2944
  tierId,
2537
2945
  ...label ? { label } : {},
@@ -2583,37 +2991,37 @@ function validateMemberContentDocument(input, options = {}) {
2583
2991
  const ids = /* @__PURE__ */ new Set();
2584
2992
  const slugs = /* @__PURE__ */ new Set();
2585
2993
  const pages = [];
2586
- for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2587
- const path = `document.pages[${index}]`;
2588
- const raw = record(value);
2589
- if (!raw) {
2994
+ for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2995
+ const path = `document.pages[${index2}]`;
2996
+ const raw2 = record(value);
2997
+ if (!raw2) {
2590
2998
  errors.push(`${path} must be an object`);
2591
2999
  continue;
2592
3000
  }
2593
- unknownKeys(raw, PAGE_KEYS, path, errors);
2594
- const id5 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2595
- const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2596
- const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2597
- if (id5 && !ID.test(id5)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2598
- if (slug && !SLUG7.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2599
- if (ids.has(id5) || knownParentIds.has(id5)) errors.push(`${path}.id duplicates page "${id5}"`);
3001
+ unknownKeys(raw2, PAGE_KEYS, path, errors);
3002
+ const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
3003
+ const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
3004
+ const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
3005
+ if (id6 && !ID.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3006
+ if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
3007
+ if (ids.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
2600
3008
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2601
- ids.add(id5);
3009
+ ids.add(id6);
2602
3010
  slugs.add(slug);
2603
- const navRaw = record(raw.navigation);
2604
- let navigation = { section: "General", order: index };
3011
+ const navRaw = record(raw2.navigation);
3012
+ let navigation = { section: "General", order: index2 };
2605
3013
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
2606
3014
  else {
2607
3015
  unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2608
3016
  const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2609
- const order = navRaw.order;
2610
- if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
3017
+ const order2 = navRaw.order;
3018
+ if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
2611
3019
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2612
3020
  }
2613
3021
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2614
- navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
3022
+ navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
2615
3023
  }
2616
- const audienceRaw = record(raw.audience);
3024
+ const audienceRaw = record(raw2.audience);
2617
3025
  let audience = { kind: "all-members" };
2618
3026
  if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2619
3027
  else {
@@ -2635,12 +3043,12 @@ function validateMemberContentDocument(input, options = {}) {
2635
3043
  audience = { kind: "tiers", tierIds: normalized };
2636
3044
  } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2637
3045
  }
2638
- const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2639
- const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
2640
- const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
3046
+ const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
3047
+ const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
3048
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
2641
3049
  let upsell;
2642
- if (raw.upsell !== void 0) {
2643
- const upsellRaw = record(raw.upsell);
3050
+ if (raw2.upsell !== void 0) {
3051
+ const upsellRaw = record(raw2.upsell);
2644
3052
  if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2645
3053
  else {
2646
3054
  unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
@@ -2654,13 +3062,13 @@ function validateMemberContentDocument(input, options = {}) {
2654
3062
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2655
3063
  }
2656
3064
  }
2657
- pages.push({ id: id5, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
3065
+ pages.push({ id: id6, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2658
3066
  }
2659
3067
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2660
- for (const [index, page] of pages.entries()) {
3068
+ for (const [index2, page] of pages.entries()) {
2661
3069
  const parentId = page.navigation.parentId;
2662
- if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2663
- if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
3070
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
3071
+ if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
2664
3072
  }
2665
3073
  for (const page of pages) {
2666
3074
  const seen = /* @__PURE__ */ new Set([page.id]);
@@ -2681,21 +3089,21 @@ function validateMemberContentDocument(input, options = {}) {
2681
3089
  const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2682
3090
  const seen = /* @__PURE__ */ new Set();
2683
3091
  upsellOverrides = [];
2684
- for (const [index, value] of values.entries()) {
2685
- const path = `document.upsellOverrides[${index}]`;
2686
- const raw = record(value);
2687
- if (!raw) {
3092
+ for (const [index2, value] of values.entries()) {
3093
+ const path = `document.upsellOverrides[${index2}]`;
3094
+ const raw2 = record(value);
3095
+ if (!raw2) {
2688
3096
  errors.push(`${path} must be an object`);
2689
3097
  continue;
2690
3098
  }
2691
- unknownKeys(raw, OVERRIDE_KEYS, path, errors);
2692
- const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
3099
+ unknownKeys(raw2, OVERRIDE_KEYS, path, errors);
3100
+ const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2693
3101
  if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2694
3102
  seen.add(pageId);
2695
3103
  const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2696
3104
  if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2697
3105
  else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2698
- const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
3106
+ const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2699
3107
  if (offer) upsellOverrides.push({ pageId, offer });
2700
3108
  }
2701
3109
  }
@@ -2820,7 +3228,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
2820
3228
  function unknownKeys2(value, allowed2, path, errors) {
2821
3229
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2822
3230
  }
2823
- function text(value, path, errors, max = MAX_TEXT2, required = true) {
3231
+ function text2(value, path, errors, max = MAX_TEXT2, required = true) {
2824
3232
  if (value === void 0 && !required) return void 0;
2825
3233
  if (typeof value !== "string" || required && !value.trim()) {
2826
3234
  errors.push(`${path} must be a non-empty string`);
@@ -2830,23 +3238,23 @@ function text(value, path, errors, max = MAX_TEXT2, required = true) {
2830
3238
  if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
2831
3239
  return result;
2832
3240
  }
2833
- function integer(value, path, errors, minimum = 1) {
3241
+ function integer2(value, path, errors, minimum = 1) {
2834
3242
  if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
2835
3243
  return Number(value);
2836
3244
  }
2837
3245
  function scopeValue(value, path, errors) {
2838
- const raw = record2(value);
2839
- if (!raw) {
3246
+ const raw2 = record2(value);
3247
+ if (!raw2) {
2840
3248
  errors.push(`${path} must be an object`);
2841
3249
  return null;
2842
3250
  }
2843
- unknownKeys2(raw, SCOPE_KEYS, path, errors);
2844
- if (raw.kind === "global" || raw.kind === "built_not_found") {
2845
- if (raw.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
2846
- return { kind: raw.kind };
3251
+ unknownKeys2(raw2, SCOPE_KEYS, path, errors);
3252
+ if (raw2.kind === "global" || raw2.kind === "built_not_found") {
3253
+ if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
3254
+ return { kind: raw2.kind };
2847
3255
  }
2848
- if (raw.kind === "chapter") {
2849
- const chapterId = text(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3256
+ if (raw2.kind === "chapter") {
3257
+ const chapterId = text2(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
2850
3258
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
2851
3259
  return { kind: "chapter", chapterId };
2852
3260
  }
@@ -2854,28 +3262,28 @@ function scopeValue(value, path, errors) {
2854
3262
  return null;
2855
3263
  }
2856
3264
  function resourceValue(value, path, errors) {
2857
- const raw = record2(value);
2858
- if (!raw) {
3265
+ const raw2 = record2(value);
3266
+ if (!raw2) {
2859
3267
  errors.push(`${path} must be an object`);
2860
3268
  return null;
2861
3269
  }
2862
- unknownKeys2(raw, RESOURCE_KEYS, path, errors);
2863
- const id5 = text(raw.id, `${path}.id`, errors, 80) ?? "";
2864
- if (id5 && !ID2.test(id5)) errors.push(`${path}.id must be lowercase kebab-case`);
2865
- const kind = text(raw.kind, `${path}.kind`, errors) ?? "";
3270
+ unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
3271
+ const id6 = text2(raw2.id, `${path}.id`, errors, 80) ?? "";
3272
+ if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
3273
+ const kind = text2(raw2.kind, `${path}.kind`, errors) ?? "";
2866
3274
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
2867
- const title = text(raw.title, `${path}.title`, errors, 240) ?? "";
2868
- const summary = text(raw.summary, `${path}.summary`, errors, 1e3, false);
2869
- const status = text(raw.status, `${path}.status`, errors) ?? "";
3275
+ const title = text2(raw2.title, `${path}.title`, errors, 240) ?? "";
3276
+ const summary = text2(raw2.summary, `${path}.summary`, errors, 1e3, false);
3277
+ const status = text2(raw2.status, `${path}.status`, errors) ?? "";
2870
3278
  if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
2871
- const revision = integer(raw.revision, `${path}.revision`, errors);
2872
- const scope = scopeValue(raw.scope, `${path}.scope`, errors) ?? { kind: "global" };
2873
- const tags = Array.isArray(raw.tags) ? raw.tags.map((tag, index) => text(tag, `${path}.tags[${index}]`, errors, 80) ?? "") : [];
2874
- if (!Array.isArray(raw.tags)) errors.push(`${path}.tags must be an array`);
3279
+ const revision = integer2(raw2.revision, `${path}.revision`, errors);
3280
+ const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
3281
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text2(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
3282
+ if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
2875
3283
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
2876
3284
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
2877
- const bodyMarkdown = text(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
2878
- const url = text(raw.url, `${path}.url`, errors, 2048, false);
3285
+ const bodyMarkdown = text2(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
3286
+ const url = text2(raw2.url, `${path}.url`, errors, 2048, false);
2879
3287
  if (url) {
2880
3288
  try {
2881
3289
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -2883,13 +3291,13 @@ function resourceValue(value, path, errors) {
2883
3291
  errors.push(`${path}.url must be a valid https URL`);
2884
3292
  }
2885
3293
  }
2886
- const asset = raw.brandAsset === void 0 ? null : record2(raw.brandAsset);
2887
- if (raw.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
3294
+ const asset = raw2.brandAsset === void 0 ? null : record2(raw2.brandAsset);
3295
+ if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
2888
3296
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
2889
3297
  const brandAsset = asset ? {
2890
- bookId: text(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
2891
- assetId: text(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
2892
- ...asset.filename === void 0 ? {} : { filename: text(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
3298
+ bookId: text2(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
3299
+ assetId: text2(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
3300
+ ...asset.filename === void 0 ? {} : { filename: text2(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
2893
3301
  } : void 0;
2894
3302
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
2895
3303
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -2897,59 +3305,59 @@ function resourceValue(value, path, errors) {
2897
3305
  if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
2898
3306
  if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
2899
3307
  if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
2900
- const provenanceRaw = record2(raw.provenance);
3308
+ const provenanceRaw = record2(raw2.provenance);
2901
3309
  if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
2902
3310
  if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
2903
3311
  const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
2904
3312
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
2905
3313
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
2906
3314
  const promotedFrom = promotedRaw ? {
2907
- chapterId: text(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
2908
- resourceId: text(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
2909
- resourceRevision: integer(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
2910
- documentRevision: integer(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
2911
- promotedBy: text(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
2912
- promotedAt: integer(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
3315
+ chapterId: text2(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
3316
+ resourceId: text2(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
3317
+ resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
3318
+ documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
3319
+ promotedBy: text2(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3320
+ promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
2913
3321
  } : void 0;
2914
3322
  const provenance = {
2915
- createdBy: text(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
2916
- createdAt: integer(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
2917
- updatedBy: text(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
2918
- updatedAt: integer(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
3323
+ createdBy: text2(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3324
+ createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
3325
+ updatedBy: text2(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3326
+ updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
2919
3327
  ...promotedFrom ? { promotedFrom } : {}
2920
3328
  };
2921
- return { id: id5, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
3329
+ return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
2922
3330
  }
2923
3331
  function validateBuilderLibraryDocument(value, options = {}) {
2924
3332
  const errors = [];
2925
- const raw = record2(value);
2926
- if (!raw) return { ok: false, errors: ["document must be an object"] };
2927
- unknownKeys2(raw, ROOT_KEYS2, "document", errors);
2928
- if (raw.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2929
- const revision = integer(raw.revision, "document.revision", errors);
2930
- const generatedAt = integer(raw.generatedAt, "document.generatedAt", errors, 0);
2931
- const sourceRaw = record2(raw.source);
3333
+ const raw2 = record2(value);
3334
+ if (!raw2) return { ok: false, errors: ["document must be an object"] };
3335
+ unknownKeys2(raw2, ROOT_KEYS2, "document", errors);
3336
+ if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
3337
+ const revision = integer2(raw2.revision, "document.revision", errors);
3338
+ const generatedAt = integer2(raw2.generatedAt, "document.generatedAt", errors, 0);
3339
+ const sourceRaw = record2(raw2.source);
2932
3340
  if (!sourceRaw) errors.push("document.source must be an object");
2933
3341
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
2934
3342
  const sourceType = sourceRaw?.type;
2935
3343
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
2936
- const sourceAppId = text(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
3344
+ const sourceAppId = text2(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
2937
3345
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
2938
3346
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2939
3347
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2940
- const targetRaw = raw.target === void 0 ? null : record2(raw.target);
2941
- if (raw.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
3348
+ const targetRaw = raw2.target === void 0 ? null : record2(raw2.target);
3349
+ if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
2942
3350
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
2943
3351
  const target = targetRaw ? {
2944
- appId: text(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
2945
- environment: text(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3352
+ appId: text2(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3353
+ environment: text2(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
2946
3354
  } : void 0;
2947
3355
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
2948
- if (!Array.isArray(raw.resources)) errors.push("document.resources must be an array");
2949
- const inputs = Array.isArray(raw.resources) ? raw.resources : [];
3356
+ if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
3357
+ const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
2950
3358
  if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
2951
- const resources = inputs.map((item, index) => resourceValue(item, `document.resources[${index}]`, errors)).filter(Boolean);
2952
- const ids = resources.map(({ id: id5 }) => id5);
3359
+ const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
3360
+ const ids = resources.map(({ id: id6 }) => id6);
2953
3361
  if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
2954
3362
  for (const resource of resources) {
2955
3363
  if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
@@ -3066,10 +3474,10 @@ function planDelivery(input) {
3066
3474
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
3067
3475
  const to = redirect ? input.group.debugEmail : input.to;
3068
3476
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
3069
- const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
3477
+ const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
3070
3478
 
3071
3479
  ` + render(tpl.text, vars) : render(tpl.text, vars);
3072
- return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
3480
+ return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
3073
3481
  }
3074
3482
 
3075
3483
  // src/notify.ts
@@ -3104,9 +3512,9 @@ async function sendTemplated(deps, input) {
3104
3512
  error = e instanceof Error ? e.message : String(e);
3105
3513
  }
3106
3514
  }
3107
- const id5 = deps.newId();
3515
+ const id6 = deps.newId();
3108
3516
  const row = {
3109
- id: id5,
3517
+ id: id6,
3110
3518
  groupId: input.group.id,
3111
3519
  to: decision.to,
3112
3520
  template: input.template,
@@ -3120,7 +3528,7 @@ async function sendTemplated(deps, input) {
3120
3528
  ...messageId ? { messageId } : {},
3121
3529
  ...error ? { error } : {}
3122
3530
  };
3123
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id5, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
3531
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
3124
3532
  return error ? { sent: false, reason: error } : { sent: true };
3125
3533
  }
3126
3534
  function emailGroupFrom(row) {
@@ -3306,29 +3714,29 @@ function validateAdmissionGrantDocument(input) {
3306
3714
  const grants = [];
3307
3715
  const seenIds = /* @__PURE__ */ new Set();
3308
3716
  const seenEmails = /* @__PURE__ */ new Set();
3309
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
3310
- const grant = raw && typeof raw === "object" ? raw : {};
3311
- const id5 = typeof grant.id === "string" ? grant.id : "";
3717
+ for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
3718
+ const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
3719
+ const id6 = typeof grant.id === "string" ? grant.id : "";
3312
3720
  const email = typeof grant.email === "string" ? grant.email.trim() : "";
3313
3721
  const normalized = normalizeAdmissionEmail(email);
3314
3722
  const decision = grant.decision;
3315
3723
  const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
3316
3724
  const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
3317
- if (!ID3.test(id5)) errors.push(`document.grants[${index}].id must be an identifier`);
3318
- if (seenIds.has(id5)) errors.push(`document.grants repeats id "${id5}"`);
3319
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
3725
+ if (!ID3.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
3726
+ if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
3727
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
3320
3728
  if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
3321
3729
  if (decision !== "waive_interview" && decision !== "require_interview") {
3322
- errors.push(`document.grants[${index}].decision is invalid`);
3730
+ errors.push(`document.grants[${index2}].decision is invalid`);
3323
3731
  }
3324
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
3732
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
3325
3733
  if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
3326
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
3734
+ errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
3327
3735
  }
3328
- seenIds.add(id5);
3736
+ seenIds.add(id6);
3329
3737
  seenEmails.add(normalized);
3330
3738
  grants.push({
3331
- id: id5,
3739
+ id: id6,
3332
3740
  email,
3333
3741
  decision,
3334
3742
  ...note ? { note } : {},
@@ -3399,8 +3807,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3399
3807
  const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
3400
3808
  const ops = [];
3401
3809
  for (const row of current) {
3402
- const id5 = String(row.id ?? "");
3403
- if (id5 && !incomingIds.has(id5)) ops.push({ t: "update", ns: "admissionGrants", id: id5, attrs: {
3810
+ const id6 = String(row.id ?? "");
3811
+ if (id6 && !incomingIds.has(id6)) ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
3404
3812
  active: false,
3405
3813
  revokedAt: receivedAt,
3406
3814
  revokedBy: document.source.actor,
@@ -3409,10 +3817,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3409
3817
  } });
3410
3818
  }
3411
3819
  for (const grant of document.grants) {
3412
- const id5 = `admission:parent:${document.source.appId}:${grant.id}`;
3413
- const existing = current.find((row) => row.id === id5);
3414
- ops.push({ t: "update", ns: "admissionGrants", id: id5, attrs: {
3415
- id: id5,
3820
+ const id6 = `admission:parent:${document.source.appId}:${grant.id}`;
3821
+ const existing = current.find((row) => row.id === id6);
3822
+ ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
3823
+ id: id6,
3416
3824
  sourceGrantId: grant.id,
3417
3825
  sourceScope: "parent",
3418
3826
  sourceId: document.source.appId,
@@ -3433,7 +3841,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3433
3841
  ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
3434
3842
  ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
3435
3843
  ];
3436
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id5, attrs: retract });
3844
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id6, attrs: retract });
3437
3845
  }
3438
3846
  if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
3439
3847
  const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
@@ -3475,7 +3883,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
3475
3883
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3476
3884
  var CURRENCY = /^[a-z]{3}$/;
3477
3885
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
3478
- var text2 = (value, path, errors, max) => {
3886
+ var text3 = (value, path, errors, max) => {
3479
3887
  if (typeof value !== "string" || !value.trim()) {
3480
3888
  errors.push(`${path} is required`);
3481
3889
  return "";
@@ -3494,32 +3902,32 @@ function validateGiftOfferCatalog(input, tierIds) {
3494
3902
  if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
3495
3903
  const ids = /* @__PURE__ */ new Set();
3496
3904
  const offers = [];
3497
- for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
3498
- const path = `document.giftOffers.offers[${index}]`;
3499
- const item = record3(raw);
3905
+ for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
3906
+ const path = `document.giftOffers.offers[${index2}]`;
3907
+ const item = record3(raw2);
3500
3908
  if (!item) {
3501
3909
  errors.push(`${path} must be an object`);
3502
3910
  continue;
3503
3911
  }
3504
3912
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
3505
3913
  for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
3506
- const id5 = text2(item.id, `${path}.id`, errors, 80);
3507
- if (id5 && !ID4.test(id5)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3508
- if (ids.has(id5)) errors.push(`${path}.id duplicates "${id5}"`);
3509
- ids.add(id5);
3914
+ const id6 = text3(item.id, `${path}.id`, errors, 80);
3915
+ if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3916
+ if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
3917
+ ids.add(id6);
3510
3918
  const targetRaw = record3(item.target);
3511
3919
  let target = { type: "entitlement", entitlementKey: "invalid" };
3512
3920
  if (targetRaw?.type === "tier") {
3513
- const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3921
+ const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3514
3922
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
3515
3923
  target = { type: "tier", tierId };
3516
3924
  } else if (targetRaw?.type === "entitlement") {
3517
- const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3925
+ const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3518
3926
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
3519
3927
  target = { type: "entitlement", entitlementKey };
3520
3928
  } else errors.push(`${path}.target.type must be tier or entitlement`);
3521
3929
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
3522
- const stripePriceId = text2(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3930
+ const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3523
3931
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
3524
3932
  if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
3525
3933
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -3532,7 +3940,7 @@ function validateGiftOfferCatalog(input, tierIds) {
3532
3940
  if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
3533
3941
  if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
3534
3942
  offers.push({
3535
- id: id5,
3943
+ id: id6,
3536
3944
  target,
3537
3945
  billingMode: item.billingMode,
3538
3946
  stripePriceId,
@@ -3541,9 +3949,9 @@ function validateGiftOfferCatalog(input, tierIds) {
3541
3949
  redemptionLifetimeHours: lifetime,
3542
3950
  ...activeFrom !== void 0 ? { activeFrom } : {},
3543
3951
  ...activeUntil !== void 0 ? { activeUntil } : {},
3544
- title: text2(item.title, `${path}.title`, errors, 160),
3545
- description: text2(item.description, `${path}.description`, errors, 2e3),
3546
- ctaLabel: text2(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3952
+ title: text3(item.title, `${path}.title`, errors, 160),
3953
+ description: text3(item.description, `${path}.description`, errors, 2e3),
3954
+ ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3547
3955
  });
3548
3956
  }
3549
3957
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -3583,8 +3991,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
3583
3991
  const typeKey = type.toLowerCase();
3584
3992
  const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
3585
3993
  const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
3586
- const raw = `${prefix}:${hubRecordId}`;
3587
- if (readable && raw.length <= 64) return raw;
3994
+ const raw2 = `${prefix}:${hubRecordId}`;
3995
+ if (readable && raw2.length <= 64) return raw2;
3588
3996
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
3589
3997
  }
3590
3998
  function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
@@ -3854,14 +4262,14 @@ function periodSeries(points, now, count, periodMs) {
3854
4262
  for (const point of points) {
3855
4263
  if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
3856
4264
  if (point.t >= start && point.t <= now) {
3857
- const index = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
3858
- current[index] = (current[index] ?? 0) + point.v;
4265
+ const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
4266
+ current[index2] = (current[index2] ?? 0) + point.v;
3859
4267
  } else if (point.t >= previousStart && point.t < start) {
3860
- const index = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
3861
- previous[index] = (previous[index] ?? 0) + point.v;
4268
+ const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
4269
+ previous[index2] = (previous[index2] ?? 0) + point.v;
3862
4270
  }
3863
4271
  }
3864
- const labels = current.map((_, index) => new Date(start + index * periodMs).toISOString().slice(5, 10));
4272
+ const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
3865
4273
  return { labels, current, previous };
3866
4274
  }
3867
4275
  function dashboardMetricData(points, now) {
@@ -3945,9 +4353,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
3945
4353
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
3946
4354
  if (!found.ok) return false;
3947
4355
  const users = await found.json().catch(() => null);
3948
- const id5 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
3949
- if (!id5) return false;
3950
- return updateClerkUserMetadata(secretKey, id5, input, fetchImpl);
4356
+ const id6 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
4357
+ if (!id6) return false;
4358
+ return updateClerkUserMetadata(secretKey, id6, input, fetchImpl);
3951
4359
  }
3952
4360
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
3953
4361
  const { path, body } = clerkUserRequest(input);
@@ -4010,8 +4418,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
4010
4418
  const user = Array.isArray(data) ? data[0] : void 0;
4011
4419
  return user ? toRecord(user) : null;
4012
4420
  }
4013
- async function clerkGetUser(secretKey, id5, fetchImpl = fetch) {
4014
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id5)}`, secretKey, fetchImpl).catch(() => null);
4421
+ async function clerkGetUser(secretKey, id6, fetchImpl = fetch) {
4422
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id6)}`, secretKey, fetchImpl).catch(() => null);
4015
4423
  return data ? toRecord(data) : null;
4016
4424
  }
4017
4425
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -4027,8 +4435,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
4027
4435
  }
4028
4436
  return out;
4029
4437
  }
4030
- async function clerkSetRole(secretKey, id5, role, fetchImpl = fetch) {
4031
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id5)}/metadata`, {
4438
+ async function clerkSetRole(secretKey, id6, role, fetchImpl = fetch) {
4439
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id6)}/metadata`, {
4032
4440
  method: "PATCH",
4033
4441
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
4034
4442
  body: JSON.stringify({ public_metadata: { role } })
@@ -4254,8 +4662,8 @@ function chapterBrandFromTokens(compiled, base = {}) {
4254
4662
  function isReconcilable(meeting, now) {
4255
4663
  return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
4256
4664
  }
4257
- function reconcileMeetings(meetings2, events, now) {
4258
- const byEvent = new Map(events.map((e) => [e.eventId, e]));
4665
+ function reconcileMeetings(meetings2, events2, now) {
4666
+ const byEvent = new Map(events2.map((e) => [e.eventId, e]));
4259
4667
  const decisions = [];
4260
4668
  for (const m of meetings2) {
4261
4669
  if (!isReconcilable(m, now) || !m.googleEventId) continue;
@@ -4465,10 +4873,10 @@ function boundedString(value, path, errors, max, required = false) {
4465
4873
  errors.push(`${path} must be a string`);
4466
4874
  return void 0;
4467
4875
  }
4468
- const text4 = value.trim();
4469
- if (required && !text4) errors.push(`${path} is required`);
4470
- if (text4.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4471
- return text4;
4876
+ const text5 = value.trim();
4877
+ if (required && !text5) errors.push(`${path} is required`);
4878
+ if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4879
+ return text5;
4472
4880
  }
4473
4881
  function validateMembershipPageDocument(input, options = {}) {
4474
4882
  const errors = [];
@@ -4486,9 +4894,9 @@ function validateMembershipPageDocument(input, options = {}) {
4486
4894
  const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
4487
4895
  const seen = /* @__PURE__ */ new Set();
4488
4896
  const tiers2 = [];
4489
- for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
4490
- const path = `document.tiers[${index}]`;
4491
- const item = objectRecord(raw);
4897
+ for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
4898
+ const path = `document.tiers[${index2}]`;
4899
+ const item = objectRecord(raw2);
4492
4900
  if (!item) {
4493
4901
  errors.push(`${path} must be an object`);
4494
4902
  continue;
@@ -4517,8 +4925,8 @@ function validateMembershipPageDocument(input, options = {}) {
4517
4925
  else {
4518
4926
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
4519
4927
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
4520
- const text4 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4521
- return text4 ? [text4] : [];
4928
+ const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4929
+ return text5 ? [text5] : [];
4522
4930
  });
4523
4931
  }
4524
4932
  }
@@ -4594,7 +5002,7 @@ function membershipPageDocument(input, options = {}) {
4594
5002
  }
4595
5003
 
4596
5004
  // src/signup-control-validate.ts
4597
- var SLUG8 = /^[a-z0-9][a-z0-9-]{1,62}$/;
5005
+ var SLUG9 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4598
5006
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
4599
5007
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
4600
5008
  var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
@@ -4617,7 +5025,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
4617
5025
  function unknownKeys3(value, allowed2, path, errors) {
4618
5026
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
4619
5027
  }
4620
- function text3(value, path, errors, max = 65536) {
5028
+ function text4(value, path, errors, max = 65536) {
4621
5029
  if (typeof value !== "string" || !value.trim()) {
4622
5030
  errors.push(`${path} is required`);
4623
5031
  return "";
@@ -4632,19 +5040,19 @@ function tiersOf(value, errors) {
4632
5040
  }
4633
5041
  if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
4634
5042
  const seen = /* @__PURE__ */ new Set();
4635
- return value.slice(0, 20).flatMap((raw, index) => {
4636
- const path = `document.tiers[${index}]`;
4637
- const item = recordOf(raw);
5043
+ return value.slice(0, 20).flatMap((raw2, index2) => {
5044
+ const path = `document.tiers[${index2}]`;
5045
+ const item = recordOf(raw2);
4638
5046
  if (!item) {
4639
5047
  errors.push(`${path} must be an object`);
4640
5048
  return [];
4641
5049
  }
4642
5050
  unknownKeys3(item, TIER_KEYS2, path, errors);
4643
- const id5 = text3(item.id, `${path}.id`, errors, 80);
4644
- if (id5 && !TIER.test(id5)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4645
- if (seen.has(id5)) errors.push(`${path}.id duplicates "${id5}"`);
4646
- seen.add(id5);
4647
- const name = text3(item.name, `${path}.name`, errors, 160);
5051
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
5052
+ if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
5053
+ if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
5054
+ seen.add(id6);
5055
+ const name = text4(item.name, `${path}.name`, errors, 160);
4648
5056
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
4649
5057
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
4650
5058
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -4656,9 +5064,9 @@ function tiersOf(value, errors) {
4656
5064
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
4657
5065
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
4658
5066
  }
4659
- const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
5067
+ const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
4660
5068
  return [{
4661
- id: id5,
5069
+ id: id6,
4662
5070
  name,
4663
5071
  available: item.available === true,
4664
5072
  sortOrder: Number(item.sortOrder) || 0,
@@ -4712,7 +5120,7 @@ function policyOf(value, errors) {
4712
5120
  const result = {};
4713
5121
  for (const key of allowed2) {
4714
5122
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
4715
- result[key] = text3(input[key], `document.policy.${key}`, errors);
5123
+ result[key] = text4(input[key], `document.policy.${key}`, errors);
4716
5124
  }
4717
5125
  return result;
4718
5126
  }
@@ -4750,18 +5158,18 @@ function validateSignupControlDocument(input) {
4750
5158
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
4751
5159
  const source = recordOf(root.source);
4752
5160
  const target = recordOf(root.target);
4753
- const appId = text3(source?.appId, "document.source.appId", errors, 63);
4754
- const chapterId = text3(target?.chapterId, "document.target.chapterId", errors, 63);
4755
- const environment = text3(target?.environment, "document.target.environment", errors, 63);
4756
- if (appId && !SLUG8.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
4757
- if (chapterId && !SLUG8.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
5161
+ const appId = text4(source?.appId, "document.source.appId", errors, 63);
5162
+ const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
5163
+ const environment = text4(target?.environment, "document.target.environment", errors, 63);
5164
+ if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
5165
+ if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
4758
5166
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
4759
5167
  if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
4760
5168
  const tiers2 = tiersOf(root.tiers, errors);
4761
5169
  const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
4762
5170
  const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
4763
5171
  if (!page.ok) errors.push(...page.errors);
4764
- else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
5172
+ else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
4765
5173
  errors.push("document.membershipPage.tiers must follow the available tier display order");
4766
5174
  }
4767
5175
  const application = applicationOf(root.application, errors);
@@ -4808,14 +5216,14 @@ function validatedTokens(input) {
4808
5216
  }
4809
5217
  const tokens = {};
4810
5218
  let serializedLength = 0;
4811
- for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
5219
+ for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
4812
5220
  if (!TOKEN_NAME.test(name)) {
4813
5221
  throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
4814
5222
  }
4815
- if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
5223
+ if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
4816
5224
  throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
4817
5225
  }
4818
- const value = String(raw).trim();
5226
+ const value = String(raw2).trim();
4819
5227
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
4820
5228
  throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
4821
5229
  }
@@ -4922,16 +5330,16 @@ function markdownSection(rawHeading) {
4922
5330
  }
4923
5331
  function markdownBenefits(lines, path, errors) {
4924
5332
  const benefits = [];
4925
- for (const [index, line] of lines.entries()) {
5333
+ for (const [index2, line] of lines.entries()) {
4926
5334
  if (!line.trim()) continue;
4927
5335
  const match = line.match(MARKDOWN_BENEFIT);
4928
5336
  if (!match) {
4929
- errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
5337
+ errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
4930
5338
  continue;
4931
5339
  }
4932
- const text4 = (match[1] ?? "").trim();
4933
- if (text4.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
4934
- if (text4) benefits.push(text4);
5340
+ const text5 = (match[1] ?? "").trim();
5341
+ if (text5.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
5342
+ if (text5) benefits.push(text5);
4935
5343
  }
4936
5344
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
4937
5345
  return benefits.slice(0, MAX_BENEFITS2);
@@ -4946,7 +5354,7 @@ function applyMembershipTierMarkdown(presentation, source) {
4946
5354
  const sections = /* @__PURE__ */ new Map();
4947
5355
  const prelude = [];
4948
5356
  let current = null;
4949
- for (const [index, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
5357
+ for (const [index2, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
4950
5358
  const heading = line.match(MARKDOWN_SECTION);
4951
5359
  if (heading) {
4952
5360
  const headingText = heading[1] ?? "";
@@ -4966,7 +5374,7 @@ function applyMembershipTierMarkdown(presentation, source) {
4966
5374
  continue;
4967
5375
  }
4968
5376
  if (MARKDOWN_HEADING.test(line)) {
4969
- errors.push(`membership Markdown line ${index + 1} uses an unsupported heading`);
5377
+ errors.push(`membership Markdown line ${index2 + 1} uses an unsupported heading`);
4970
5378
  continue;
4971
5379
  }
4972
5380
  if (current) sections.get(current)?.lines.push(line);
@@ -5031,7 +5439,7 @@ var exclusions = (chapterId, paidTierIds) => [
5031
5439
  id: chapterId,
5032
5440
  reason: "provider_identity",
5033
5441
  paths: [
5034
- ...paidTierIds.map((id5) => `tiers[${id5}].stripePriceId`),
5442
+ ...paidTierIds.map((id6) => `tiers[${id6}].stripePriceId`),
5035
5443
  "groups.stripePublishableKey",
5036
5444
  "applications.stripeCustomerId",
5037
5445
  "applications.stripeSubscriptionId",
@@ -5073,9 +5481,9 @@ async function exportChapterPromotion(input) {
5073
5481
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
5074
5482
  receiptDigests: [input.digest]
5075
5483
  };
5076
- const artifact = (kind, id5, content) => ({
5484
+ const artifact = (kind, id6, content) => ({
5077
5485
  kind,
5078
- id: id5,
5486
+ id: id6,
5079
5487
  sourceRevision: revision,
5080
5488
  provenance: promotionProvenance,
5081
5489
  content: (0, import_promotion.canonicalPromotionValue)(content)
@@ -5114,7 +5522,7 @@ async function exportChapterPromotion(input) {
5114
5522
  }
5115
5523
 
5116
5524
  // src/member-system-authority.ts
5117
- var rows2 = (group) => group.fields.map((id5) => ({ ...group, id: id5 }));
5525
+ var rows2 = (group) => group.fields.map((id6) => ({ ...group, id: id6 }));
5118
5526
  var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
5119
5527
  ...rows2({
5120
5528
  scope: "network",
@@ -5499,10 +5907,10 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
5499
5907
  if (supplied.length === keys.length) return "apply";
5500
5908
  return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
5501
5909
  }
5502
- function planMemberSystemEventReplay(events) {
5910
+ function planMemberSystemEventReplay(events2) {
5503
5911
  const seen = /* @__PURE__ */ new Map();
5504
5912
  const plan = { accepted: [], replayed: [], collisions: [] };
5505
- for (const event of events) {
5913
+ for (const event of events2) {
5506
5914
  const digest = seen.get(event.id);
5507
5915
  if (digest === void 0) {
5508
5916
  seen.set(event.id, event.digest);