@odla-ai/chapter 0.44.0 → 0.46.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
@@ -61,6 +61,7 @@ __export(index_exports, {
61
61
  brandTokens: () => brandTokens,
62
62
  bucketSeries: () => bucketSeries,
63
63
  buildBuilderLibraryManifest: () => buildBuilderLibraryManifest,
64
+ buildChapterCommercialParitySnapshot: () => buildChapterCommercialParitySnapshot,
64
65
  buildGroupSeed: () => buildGroupSeed,
65
66
  buildTierSeeds: () => buildTierSeeds,
66
67
  builderLibraryDigest: () => builderLibraryDigest,
@@ -86,6 +87,7 @@ __export(index_exports, {
86
87
  clerkListUsers: () => clerkListUsers,
87
88
  clerkSetRole: () => clerkSetRole,
88
89
  clerkUserRequest: () => clerkUserRequest,
90
+ commercialParityDigest: () => commercialParityDigest,
89
91
  composeMemberContent: () => composeMemberContent,
90
92
  createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
91
93
  createBuilderLibraryEnvelope: () => createBuilderLibraryEnvelope,
@@ -122,6 +124,7 @@ __export(index_exports, {
122
124
  joinConfig: () => joinConfig,
123
125
  joinConfigTiers: () => joinConfigTiers,
124
126
  leaderCrmConfig: () => leaderCrmConfig,
127
+ listStripeObjects: () => listStripeObjects,
125
128
  loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
126
129
  loadOfferableTiers: () => loadOfferableTiers,
127
130
  loadTiers: () => loadTiers,
@@ -136,9 +139,11 @@ __export(index_exports, {
136
139
  memberSystemRollbackPlan: () => memberSystemRollbackPlan,
137
140
  membershipPageDocument: () => membershipPageDocument,
138
141
  membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
142
+ networkCommercialSourceKey: () => networkCommercialSourceKey,
139
143
  networkSourceTag: () => networkSourceTag,
140
144
  normalizeAdmissionEmail: () => normalizeAdmissionEmail,
141
145
  normalizeChapterEmail: () => normalizeChapterEmail,
146
+ normalizeNetworkCommercialStripeEvent: () => normalizeNetworkCommercialStripeEvent,
142
147
  normalizeSharedRecord: () => normalizeSharedRecord,
143
148
  normalizeWebhookEvent: () => normalizeWebhookEvent,
144
149
  offerableTiers: () => offerableTiers,
@@ -151,10 +156,13 @@ __export(index_exports, {
151
156
  projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
152
157
  projectMemberContent: () => projectMemberContent,
153
158
  projectMemberSelf: () => projectMemberSelf,
159
+ projectNetworkCommercialEvents: () => projectNetworkCommercialEvents,
154
160
  projectSharedRecord: () => projectSharedRecord,
155
161
  promoteBuilderLibraryResource: () => promoteBuilderLibraryResource,
156
162
  publicGiftOffers: () => publicGiftOffers,
163
+ reconcileCommercialParitySnapshot: () => reconcileCommercialParitySnapshot,
157
164
  reconcileMeetings: () => reconcileMeetings,
165
+ reconcileStripeSnapshot: () => reconcileStripeSnapshot,
158
166
  refundedPatch: () => refundedPatch,
159
167
  render: () => render,
160
168
  renderSignupControlThemeHead: () => renderSignupControlThemeHead,
@@ -168,6 +176,7 @@ __export(index_exports, {
168
176
  resolveJoinContinuation: () => resolveJoinContinuation,
169
177
  resolveLeaderFormation: () => resolveLeaderFormation,
170
178
  resolveMemberContent: () => resolveMemberContent,
179
+ resolveNetworkCommercialReadModel: () => resolveNetworkCommercialReadModel,
171
180
  resolveNetworkPersonRegistry: () => resolveNetworkPersonRegistry,
172
181
  resolvePipeline: () => resolvePipeline,
173
182
  resolveScheduling: () => resolveScheduling,
@@ -241,7 +250,7 @@ function resolveApplication(a) {
241
250
  throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
242
251
  }
243
252
  }
244
- const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
253
+ const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
245
254
  if (duplicate) {
246
255
  throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
247
256
  }
@@ -320,8 +329,8 @@ async function submitApplication(db, chapter, fields, opts) {
320
329
  if (app.requireDisclaimerAck && !acked) {
321
330
  return { ok: false, error: "disclaimerAck is required" };
322
331
  }
323
- const id5 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
324
- const row = { id: id5, status: chapter.pipeline.initial, createdAt: opts.now };
332
+ const id6 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
333
+ const row = { id: id6, status: chapter.pipeline.initial, createdAt: opts.now };
325
334
  for (const f of [...app.required, ...app.optional]) {
326
335
  if (fieldStates[f]?.visible === false) continue;
327
336
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
@@ -345,19 +354,19 @@ async function submitApplication(db, chapter, fields, opts) {
345
354
  }
346
355
  if (acked) row.disclaimerAckAt = opts.now;
347
356
  const ops = [
348
- { t: "update", ns: "applications", id: id5, attrs: row }
357
+ { t: "update", ns: "applications", id: id6, attrs: row }
349
358
  ];
350
359
  if (opts.admissionGrant?.decision === "waive_interview") {
351
360
  ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
352
361
  consumedAt: opts.now,
353
- consumedApplicationId: id5
362
+ consumedApplicationId: id6
354
363
  } });
355
364
  }
356
365
  const { duplicate } = await db.transact(
357
366
  ops,
358
367
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
359
368
  );
360
- return { ok: true, id: id5, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
369
+ return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
361
370
  }
362
371
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
363
372
  return {
@@ -753,6 +762,64 @@ var networkPersonEntities = {
753
762
  networkPersonEvents: networkPersonEventsEntity
754
763
  };
755
764
 
765
+ // src/schema-network-commercial.ts
766
+ var string2 = (flags = {}) => ({
767
+ type: "string",
768
+ unique: flags.unique ?? false,
769
+ indexed: flags.indexed ?? false,
770
+ optional: flags.optional ?? false
771
+ });
772
+ var number2 = (flags = {}) => ({
773
+ type: "number",
774
+ unique: false,
775
+ indexed: flags.indexed ?? false,
776
+ optional: flags.optional ?? false
777
+ });
778
+ var boolean = (flags = {}) => ({
779
+ type: "boolean",
780
+ unique: false,
781
+ indexed: flags.indexed ?? false,
782
+ optional: false
783
+ });
784
+ var json2 = () => ({ type: "json", unique: false, indexed: false, optional: false });
785
+ var id4 = () => string2({ unique: true, indexed: true });
786
+ var events = { attrs: {
787
+ id: id4(),
788
+ eventId: string2({ unique: true, indexed: true }),
789
+ digest: string2({ indexed: true }),
790
+ eventType: string2({ indexed: true }),
791
+ eventCreated: number2({ indexed: true }),
792
+ livemode: boolean({ indexed: true }),
793
+ fact: json2(),
794
+ receivedAt: number2({ indexed: true })
795
+ } };
796
+ var projection = () => ({ attrs: {
797
+ id: id4(),
798
+ providerId: string2({ indexed: true }),
799
+ sourceKey: string2({ indexed: true }),
800
+ status: string2({ indexed: true }),
801
+ snapshot: json2(),
802
+ eventCreated: number2({ indexed: true }),
803
+ updatedAt: number2({ indexed: true })
804
+ } });
805
+ var exceptions = { attrs: {
806
+ id: id4(),
807
+ eventId: string2({ indexed: true }),
808
+ kind: string2({ indexed: true }),
809
+ sourceKey: string2({ indexed: true, optional: true }),
810
+ status: string2({ indexed: true }),
811
+ detail: json2(),
812
+ updatedAt: number2({ indexed: true })
813
+ } };
814
+ var networkCommercialEntities = {
815
+ networkCommercialEvents: events,
816
+ networkCommercialCustomers: projection(),
817
+ networkCommercialSubscriptions: projection(),
818
+ networkCommercialInvoices: projection(),
819
+ networkCommercialRefunds: projection(),
820
+ networkCommercialExceptions: exceptions
821
+ };
822
+
756
823
  // src/schema.ts
757
824
  function attr5(type, flags = {}) {
758
825
  return {
@@ -762,10 +829,10 @@ function attr5(type, flags = {}) {
762
829
  optional: flags.optional ?? false
763
830
  };
764
831
  }
765
- var id4 = () => attr5("string", { unique: true, indexed: true });
832
+ var id5 = () => attr5("string", { unique: true, indexed: true });
766
833
  var admins = {
767
834
  attrs: {
768
- id: id4(),
835
+ id: id5(),
769
836
  email: attr5("string", { unique: true, indexed: true }),
770
837
  name: attr5("string", { optional: true }),
771
838
  note: attr5("string", { optional: true })
@@ -773,7 +840,7 @@ var admins = {
773
840
  };
774
841
  var superAdmins = {
775
842
  attrs: {
776
- id: id4(),
843
+ id: id5(),
777
844
  email: attr5("string", { unique: true, indexed: true }),
778
845
  note: attr5("string", { optional: true }),
779
846
  createdAt: attr5("number", { indexed: true })
@@ -781,7 +848,7 @@ var superAdmins = {
781
848
  };
782
849
  var networkNotes = {
783
850
  attrs: {
784
- id: id4(),
851
+ id: id5(),
785
852
  recordKey: attr5("string", { indexed: true }),
786
853
  targetId: attr5("string", { indexed: true }),
787
854
  recordType: attr5("string", { indexed: true }),
@@ -794,7 +861,7 @@ var networkNotes = {
794
861
  };
795
862
  var signupControls = {
796
863
  attrs: {
797
- id: id4(),
864
+ id: id5(),
798
865
  sourceId: attr5("string", { indexed: true }),
799
866
  targetId: attr5("string", { indexed: true }),
800
867
  environment: attr5("string", { indexed: true }),
@@ -807,7 +874,7 @@ var signupControls = {
807
874
  };
808
875
  var memberContentControls = {
809
876
  attrs: {
810
- id: id4(),
877
+ id: id5(),
811
878
  sourceId: attr5("string", { indexed: true }),
812
879
  targetId: attr5("string", { indexed: true }),
813
880
  environment: attr5("string", { indexed: true }),
@@ -820,7 +887,7 @@ var memberContentControls = {
820
887
  };
821
888
  var memberContentLocal = {
822
889
  attrs: {
823
- id: id4(),
890
+ id: id5(),
824
891
  chapterId: attr5("string", { indexed: true }),
825
892
  environment: attr5("string", { indexed: true }),
826
893
  revision: attr5("number", { indexed: true }),
@@ -831,7 +898,7 @@ var memberContentLocal = {
831
898
  };
832
899
  var builderLibraryControls = {
833
900
  attrs: {
834
- id: id4(),
901
+ id: id5(),
835
902
  sourceId: attr5("string", { indexed: true }),
836
903
  targetId: attr5("string", { indexed: true }),
837
904
  environment: attr5("string", { indexed: true }),
@@ -844,7 +911,7 @@ var builderLibraryControls = {
844
911
  };
845
912
  var builderLibraryLocal = {
846
913
  attrs: {
847
- id: id4(),
914
+ id: id5(),
848
915
  appId: attr5("string", { indexed: true }),
849
916
  environment: attr5("string", { indexed: true }),
850
917
  revision: attr5("number", { indexed: true }),
@@ -855,7 +922,7 @@ var builderLibraryLocal = {
855
922
  };
856
923
  var groups = {
857
924
  attrs: {
858
- id: id4(),
925
+ id: id5(),
859
926
  name: attr5("string"),
860
927
  standardPriceCents: attr5("number"),
861
928
  foundingDiscountCents: attr5("number"),
@@ -878,7 +945,7 @@ var groups = {
878
945
  };
879
946
  var tiers = {
880
947
  attrs: {
881
- id: id4(),
948
+ id: id5(),
882
949
  groupId: attr5("string", { indexed: true }),
883
950
  name: attr5("string"),
884
951
  priceCents: attr5("number"),
@@ -891,7 +958,7 @@ var tiers = {
891
958
  };
892
959
  var meetings = {
893
960
  attrs: {
894
- id: id4(),
961
+ id: id5(),
895
962
  applicationId: attr5("string", { indexed: true }),
896
963
  groupId: attr5("string", { indexed: true }),
897
964
  startAt: attr5("number", { indexed: true }),
@@ -910,7 +977,7 @@ var meetings = {
910
977
  };
911
978
  var emailLog = {
912
979
  attrs: {
913
- id: id4(),
980
+ id: id5(),
914
981
  groupId: attr5("string", { indexed: true }),
915
982
  applicationId: attr5("string", { indexed: true, optional: true }),
916
983
  to: attr5("string", { indexed: true }),
@@ -925,7 +992,7 @@ var emailLog = {
925
992
  sentAt: attr5("number", { indexed: true })
926
993
  }
927
994
  };
928
- function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false) {
995
+ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false, includeNetworkCommercialReadModel = false) {
929
996
  const entities = {};
930
997
  if (mode === "chapter") {
931
998
  entities.applications = applicationsFor(application);
@@ -962,6 +1029,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
962
1029
  if (includeNetworkPersonRegistry) {
963
1030
  Object.assign(entities, networkPersonEntities);
964
1031
  }
1032
+ if (includeNetworkCommercialReadModel) Object.assign(entities, networkCommercialEntities);
965
1033
  const schema = { entities, links: {} };
966
1034
  const rules = {};
967
1035
  for (const ns of Object.keys(entities)) {
@@ -1133,8 +1201,8 @@ function resolveAuth(mode, auth) {
1133
1201
  return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
1134
1202
  }
1135
1203
  function roleFromClaim(payload, auth) {
1136
- const raw = payload[auth.claim];
1137
- return typeof raw === "string" && auth.ladder.includes(raw) ? raw : auth.ladder[0];
1204
+ const raw2 = payload[auth.claim];
1205
+ return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
1138
1206
  }
1139
1207
  function isAdminRole(role, auth) {
1140
1208
  return role === auth.adminRole;
@@ -1613,9 +1681,9 @@ var APPLICATION_LABELS = {
1613
1681
  phone: "Phone",
1614
1682
  state: "State"
1615
1683
  };
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;
1684
+ function humanize(id6) {
1685
+ const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1686
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
1619
1687
  }
1620
1688
  function applicationDefs(chapter) {
1621
1689
  try {
@@ -1624,23 +1692,23 @@ function applicationDefs(chapter) {
1624
1692
  return {};
1625
1693
  }
1626
1694
  }
1627
- function applicationType(id5, def) {
1695
+ function applicationType(id6, def) {
1628
1696
  if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
1629
- return id5 === "email" ? "email" : id5 === "linkedin" ? "url" : "string";
1697
+ return id6 === "email" ? "email" : id6 === "linkedin" ? "url" : "string";
1630
1698
  }
1631
1699
  function applicationFlowFields(chapter, application = chapter.application) {
1632
1700
  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);
1701
+ return [...application.required, ...application.optional].map((id6, order2) => {
1702
+ const def = defs[id6];
1703
+ const conditions = application.conditions[id6];
1704
+ const type = applicationType(id6, def);
1637
1705
  return {
1638
- id: id5,
1639
- label: def?.label ?? APPLICATION_LABELS[id5] ?? humanize(id5),
1706
+ id: id6,
1707
+ label: def?.label ?? APPLICATION_LABELS[id6] ?? humanize(id6),
1640
1708
  type,
1641
- required: application.required.includes(id5),
1642
- order,
1643
- maxLength: application.maxLen[id5] ?? application.defaultMaxLen,
1709
+ required: application.required.includes(id6),
1710
+ order: order2,
1711
+ maxLength: application.maxLen[id6] ?? application.defaultMaxLen,
1644
1712
  ...type === "enum" && def?.options ? { options: [...def.options] } : {},
1645
1713
  ...conditions ? { conditions: { ...conditions } } : {},
1646
1714
  ...type === "email" ? { emailIdentity: "literal" } : {}
@@ -1650,17 +1718,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
1650
1718
  function resolveFormationFlowFields(crm, formation) {
1651
1719
  if (!formation.enabled) return [];
1652
1720
  const def = crm.type(formation.type);
1653
- return [...formation.required, ...formation.optional].map((id5, order) => {
1654
- const field = def.fields[id5];
1721
+ return [...formation.required, ...formation.optional].map((id6, order2) => {
1722
+ const field = def.fields[id6];
1655
1723
  return {
1656
- id: id5,
1657
- label: field.label ?? humanize(id5),
1724
+ id: id6,
1725
+ label: field.label ?? humanize(id6),
1658
1726
  type: field.type,
1659
- required: formation.required.includes(id5),
1660
- order,
1661
- maxLength: formation.maxLen[id5] ?? formation.defaultMaxLen,
1727
+ required: formation.required.includes(id6),
1728
+ order: order2,
1729
+ maxLength: formation.maxLen[id6] ?? formation.defaultMaxLen,
1662
1730
  ...field.options ? { options: [...field.options] } : {},
1663
- ...id5 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1731
+ ...id6 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1664
1732
  };
1665
1733
  });
1666
1734
  }
@@ -1700,7 +1768,7 @@ function resolveLeaderFormation(input, crm) {
1700
1768
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1701
1769
  }
1702
1770
  }
1703
- const duplicates = [...required, ...optional].filter((field, index, fields) => fields.indexOf(field) !== index);
1771
+ const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
1704
1772
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1705
1773
  const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
1706
1774
  if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
@@ -1751,7 +1819,7 @@ var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
1751
1819
  var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
1752
1820
  var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
1753
1821
  function validateSharedNotes(opts) {
1754
- const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1822
+ const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
1755
1823
  const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type] || (opts.fields ? !opts.fields[type] : false));
1756
1824
  if (duplicate || invalid) {
1757
1825
  throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
@@ -1761,7 +1829,7 @@ function validateEditableFields(opts) {
1761
1829
  for (const [type, names] of Object.entries(opts.values)) {
1762
1830
  const readable = opts.readable?.[type];
1763
1831
  const def = opts.crm.config.types[type];
1764
- const duplicate = names.some((name, index) => names.indexOf(name) !== index);
1832
+ const duplicate = names.some((name, index2) => names.indexOf(name) !== index2);
1765
1833
  const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
1766
1834
  const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
1767
1835
  if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
@@ -1772,7 +1840,7 @@ function validateEditableFields(opts) {
1772
1840
  }
1773
1841
  }
1774
1842
  function validateStageTransitions(opts) {
1775
- const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1843
+ const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
1776
1844
  const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
1777
1845
  if (duplicate || invalid) {
1778
1846
  throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
@@ -1873,7 +1941,8 @@ function resolveNetwork(config, crm) {
1873
1941
  sharedNotes,
1874
1942
  editableFields,
1875
1943
  stageTransitions,
1876
- ...target.admissionGrants === true ? { admissionGrants: true } : {}
1944
+ ...target.admissionGrants === true ? { admissionGrants: true } : {},
1945
+ ...target.commercialParity === true ? { commercialParity: true } : {}
1877
1946
  });
1878
1947
  }
1879
1948
  const readerIds = /* @__PURE__ */ new Set();
@@ -1888,9 +1957,9 @@ function resolveNetwork(config, crm) {
1888
1957
  readerIds.add(reader.id);
1889
1958
  const readerFields = reader.fields ?? {};
1890
1959
  const entries = Object.entries(readerFields);
1891
- if (entries.length === 0 && !reader.admissionGrants) {
1960
+ if (entries.length === 0 && !reader.admissionGrants && !reader.commercialParity) {
1892
1961
  throw new Error(
1893
- `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
1962
+ `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only or commercial-parity reader`
1894
1963
  );
1895
1964
  }
1896
1965
  for (const [type, names] of entries) {
@@ -1948,7 +2017,8 @@ function resolveNetwork(config, crm) {
1948
2017
  sharedNotes,
1949
2018
  editableFields,
1950
2019
  stageTransitions,
1951
- ...reader.admissionGrants === true ? { admissionGrants: true } : {}
2020
+ ...reader.admissionGrants === true ? { admissionGrants: true } : {},
2021
+ ...reader.commercialParity === true ? { commercialParity: true } : {}
1952
2022
  });
1953
2023
  }
1954
2024
  return { targets, readers };
@@ -1962,22 +2032,50 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
1962
2032
  if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
1963
2033
  const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
1964
2034
  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))) {
2035
+ if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id6) => !SLUG2.test(id6))) {
1966
2036
  throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
1967
2037
  }
1968
- const unknown = chapterIds.filter((id5) => !targetIds.includes(id5));
2038
+ const unknown = chapterIds.filter((id6) => !targetIds.includes(id6));
1969
2039
  if (unknown.length) {
1970
- throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id5) => `"${id5}"`).join(", ")} are not configured network targets`);
2040
+ throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id6) => `"${id6}"`).join(", ")} are not configured network targets`);
1971
2041
  }
1972
2042
  return { mode: "shadow", chapterIds };
1973
2043
  }
1974
2044
 
2045
+ // src/config-network-commercial.ts
2046
+ var SLUG3 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
2047
+ var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2048
+ function networkCommercialSourceKey(source) {
2049
+ return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
2050
+ }
2051
+ function resolveNetworkCommercialReadModel(mode, input, targetIds) {
2052
+ if (!input) return null;
2053
+ if (mode !== "hub") throw new Error("defineChapter.networkCommercialReadModel: only hub mode may own the network commercial projection");
2054
+ if (input.mode !== "shadow") throw new Error('defineChapter.networkCommercialReadModel.mode: must be "shadow"');
2055
+ if (!Array.isArray(input.sources) || input.sources.length === 0) {
2056
+ throw new Error("defineChapter.networkCommercialReadModel.sources: at least one exact source is required");
2057
+ }
2058
+ const sources = input.sources.map((source) => ({ ...source }));
2059
+ for (const source of sources) {
2060
+ 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") {
2061
+ throw new Error("defineChapter.networkCommercialReadModel.sources: every source requires valid app, chapter, environment, data-environment, runtime, and Stripe mode values");
2062
+ }
2063
+ if (!targetIds.includes(source.chapterId)) {
2064
+ throw new Error(`defineChapter.networkCommercialReadModel.sources: chapter "${source.chapterId}" is not a configured network target`);
2065
+ }
2066
+ }
2067
+ if (new Set(sources.map(networkCommercialSourceKey)).size !== sources.length) {
2068
+ throw new Error("defineChapter.networkCommercialReadModel.sources: exact source partitions must be distinct");
2069
+ }
2070
+ return { mode: "shadow", sources };
2071
+ }
2072
+
1975
2073
  // src/config-signup-control.ts
1976
- var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2074
+ var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1977
2075
  var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
1978
2076
  function resolveSignupControl(input) {
1979
2077
  if (!input) return null;
1980
- if (!SLUG3.test(input.sourceId)) {
2078
+ if (!SLUG4.test(input.sourceId)) {
1981
2079
  throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
1982
2080
  }
1983
2081
  if (input.stripeMode !== "test" && input.stripeMode !== "live") {
@@ -1991,12 +2089,12 @@ function resolveSignupControl(input) {
1991
2089
  }
1992
2090
 
1993
2091
  // src/config-member-content.ts
1994
- var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2092
+ var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1995
2093
  var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
1996
2094
  function resolveMemberContent(input) {
1997
2095
  if (!input) return null;
1998
2096
  const parent = input.parent;
1999
- if (parent && !SLUG4.test(parent.sourceId)) {
2097
+ if (parent && !SLUG5.test(parent.sourceId)) {
2000
2098
  throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
2001
2099
  }
2002
2100
  const secretName = parent?.secretName?.trim() || "member_content_control_secret";
@@ -2011,22 +2109,22 @@ function resolveMemberContent(input) {
2011
2109
  }
2012
2110
 
2013
2111
  // src/config-builder-library.ts
2014
- var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2112
+ var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2015
2113
  function resolveBuilderLibrary(mode, input, network = { targets: [], readers: [] }) {
2016
2114
  if (!input) return null;
2017
2115
  if (input.sources !== void 0 && !Array.isArray(input.sources)) {
2018
2116
  throw new Error("defineChapter.builderLibrary.sources: must be an array");
2019
2117
  }
2020
2118
  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`);
2119
+ const sources = (input.sources ?? []).map((source, index2) => {
2120
+ if (!source || typeof source !== "object" || !SLUG6.test(source.sourceId)) {
2121
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}].sourceId: must be a lowercase app slug`);
2024
2122
  }
2025
2123
  if (source.sourceId === "built-not-found" && mode === "hub") {
2026
- throw new Error(`defineChapter.builderLibrary.sources[${index}]: a hub accepts Chapter sources, not itself`);
2124
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}]: a hub accepts Chapter sources, not itself`);
2027
2125
  }
2028
2126
  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`);
2127
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}]: Chapters may accept only the built-not-found source`);
2030
2128
  }
2031
2129
  if (seen.has(source.sourceId)) {
2032
2130
  throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
@@ -2034,14 +2132,14 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
2034
2132
  seen.add(source.sourceId);
2035
2133
  if ("secretName" in source) {
2036
2134
  throw new Error(
2037
- `defineChapter.builderLibrary.sources[${index}].secretName: remove this field; Builder Library reuses the Chapter network edge`
2135
+ `defineChapter.builderLibrary.sources[${index2}].secretName: remove this field; Builder Library reuses the Chapter network edge`
2038
2136
  );
2039
2137
  }
2040
2138
  const networkPeer = mode === "hub" ? network.targets.some((target) => target.id === source.sourceId) : network.readers.some((reader) => reader.id === source.sourceId);
2041
2139
  if (!networkPeer) {
2042
2140
  const side = mode === "hub" ? "targets" : "readers";
2043
2141
  throw new Error(
2044
- `defineChapter.builderLibrary.sources[${index}]: "${source.sourceId}" must already be configured in network.${side}`
2142
+ `defineChapter.builderLibrary.sources[${index2}]: "${source.sourceId}" must already be configured in network.${side}`
2045
2143
  );
2046
2144
  }
2047
2145
  return { sourceId: source.sourceId };
@@ -2050,15 +2148,15 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
2050
2148
  }
2051
2149
 
2052
2150
  // src/config.ts
2053
- var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2151
+ var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2054
2152
  function isResolvedCrm(x) {
2055
2153
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
2056
2154
  }
2057
2155
  function defineChapter(config) {
2058
2156
  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)}`);
2157
+ const { id: id6, name } = config;
2158
+ if (typeof id6 !== "string" || !SLUG7.test(id6)) {
2159
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id6)}`);
2062
2160
  }
2063
2161
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
2064
2162
  const mode = config.mode ?? "chapter";
@@ -2086,6 +2184,11 @@ function defineChapter(config) {
2086
2184
  config.networkPersonRegistry,
2087
2185
  network.targets.map((target) => target.id)
2088
2186
  );
2187
+ const networkCommercialReadModel = resolveNetworkCommercialReadModel(
2188
+ mode,
2189
+ config.networkCommercialReadModel,
2190
+ network.targets.map((target) => target.id)
2191
+ );
2089
2192
  const signupControl = resolveSignupControl(config.signupControl);
2090
2193
  const memberContent = resolveMemberContent(config.memberContent);
2091
2194
  const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary, network);
@@ -2105,7 +2208,8 @@ function defineChapter(config) {
2105
2208
  Boolean(memberContent),
2106
2209
  Boolean(builderLibrary),
2107
2210
  network.targets.some((target) => target.admissionGrants),
2108
- Boolean(networkPersonRegistry)
2211
+ Boolean(networkPersonRegistry),
2212
+ Boolean(networkCommercialReadModel)
2109
2213
  );
2110
2214
  const services = config.services ?? ["db", "calendar", "o11y"];
2111
2215
  const account = config.account ?? "none";
@@ -2155,7 +2259,7 @@ function defineChapter(config) {
2155
2259
  };
2156
2260
  const chapter = {
2157
2261
  config,
2158
- id: id5,
2262
+ id: id6,
2159
2263
  name,
2160
2264
  mode,
2161
2265
  brand,
@@ -2165,6 +2269,7 @@ function defineChapter(config) {
2165
2269
  memberContent,
2166
2270
  builderLibrary,
2167
2271
  networkPersonRegistry,
2272
+ networkCommercialReadModel,
2168
2273
  formation,
2169
2274
  crm,
2170
2275
  auth,
@@ -2182,6 +2287,447 @@ function defineChapter(config) {
2182
2287
  return chapter;
2183
2288
  }
2184
2289
 
2290
+ // src/network-commercial.ts
2291
+ var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2292
+ var text = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
2293
+ var integer = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
2294
+ var bool = (value) => typeof value === "boolean" ? value : void 0;
2295
+ var providerId = (value, prefix) => {
2296
+ const valueText = text(value);
2297
+ return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
2298
+ };
2299
+ function metadataCandidates(object) {
2300
+ const parent = raw(object.parent);
2301
+ const subscriptionDetails = raw(object.subscription_details);
2302
+ const parentSubscription = raw(parent.subscription_details);
2303
+ return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
2304
+ }
2305
+ function metadataValue(candidates, key) {
2306
+ return candidates.map((candidate) => text(candidate[key])).find(Boolean);
2307
+ }
2308
+ function refId(value, prefix) {
2309
+ if (typeof value === "string") return providerId(value, prefix);
2310
+ return providerId(raw(value).id, prefix);
2311
+ }
2312
+ function subscriptionRef(object) {
2313
+ return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
2314
+ }
2315
+ function firstPrice(object) {
2316
+ const data = raw(object.items).data;
2317
+ const item = Array.isArray(data) ? raw(data[0]) : {};
2318
+ const priceId = refId(item.price, "price_");
2319
+ const quantity = integer(item.quantity);
2320
+ return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
2321
+ }
2322
+ function normalizeNetworkCommercialStripeEvent(input, digest) {
2323
+ const event = raw(input);
2324
+ const eventId = providerId(event.id, "evt_") ?? "";
2325
+ const eventType = text(event.type) ?? "";
2326
+ const object = raw(raw(event.data).object);
2327
+ const objectId = providerId(object.id) ?? "";
2328
+ const candidates = metadataCandidates(object);
2329
+ const identity = {};
2330
+ const identityKeys = {
2331
+ appId: "odlaAppId",
2332
+ chapterId: "chapterId",
2333
+ environment: "odlaEnvironment",
2334
+ dataEnvironment: "odlaDataEnvironment",
2335
+ runtime: "odlaRuntime"
2336
+ };
2337
+ for (const [target, source] of Object.entries(identityKeys)) {
2338
+ const value = metadataValue(candidates, source);
2339
+ if (value) identity[target] = value;
2340
+ }
2341
+ let objectKind = "ignored";
2342
+ if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
2343
+ else if (eventType.startsWith("customer.")) objectKind = "customer";
2344
+ else if (eventType.startsWith("invoice.")) objectKind = "invoice";
2345
+ else if (eventType.startsWith("refund.")) objectKind = "refund";
2346
+ else if (eventType === "charge.refunded") objectKind = "refund";
2347
+ const customerId = refId(object.customer, "cus_");
2348
+ const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
2349
+ const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
2350
+ const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
2351
+ const paymentIntentId = refId(object.payment_intent, "pi_");
2352
+ const price = firstPrice(object);
2353
+ const amountCents = integer(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
2354
+ const currency = text(object.currency)?.toLowerCase();
2355
+ const status = text(object.status);
2356
+ return {
2357
+ eventId,
2358
+ digest,
2359
+ eventType,
2360
+ eventCreated: integer(event.created) ?? 0,
2361
+ livemode: event.livemode === true,
2362
+ objectKind,
2363
+ objectId,
2364
+ identity,
2365
+ ...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
2366
+ ...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
2367
+ ...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
2368
+ ...customerId ? { customerId } : {},
2369
+ ...subscriptionId ? { subscriptionId } : {},
2370
+ ...invoiceId ? { invoiceId } : {},
2371
+ ...chargeId ? { chargeId } : {},
2372
+ ...paymentIntentId ? { paymentIntentId } : {},
2373
+ ...price,
2374
+ ...status ? { status } : {},
2375
+ ...currency ? { currency } : {},
2376
+ ...amountCents !== void 0 ? { amountCents } : {},
2377
+ ...integer(object.amount_paid) !== void 0 ? { amountPaidCents: integer(object.amount_paid) } : {},
2378
+ ...integer(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer(object.amount_remaining) } : {},
2379
+ ...integer(object.current_period_start) !== void 0 ? { currentPeriodStart: integer(object.current_period_start) } : {},
2380
+ ...integer(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer(object.current_period_end) } : {},
2381
+ ...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
2382
+ };
2383
+ }
2384
+ var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2385
+ function directSource(fact, sources) {
2386
+ const mode = fact.livemode ? "live" : "test";
2387
+ const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
2388
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
2389
+ if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2390
+ if (supplied.length > 0 && candidates.length === 0) return "wrong";
2391
+ return null;
2392
+ }
2393
+ var objectRefs = (fact) => [
2394
+ fact.customerId && `customer:${fact.customerId}`,
2395
+ fact.subscriptionId && `subscription:${fact.subscriptionId}`,
2396
+ fact.invoiceId && `invoice:${fact.invoiceId}`,
2397
+ fact.chargeId && `charge:${fact.chargeId}`,
2398
+ fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
2399
+ ].filter((value) => Boolean(value));
2400
+ function ownRef(fact) {
2401
+ if (!fact.objectId) return null;
2402
+ if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
2403
+ if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
2404
+ if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
2405
+ if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
2406
+ return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
2407
+ }
2408
+ var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
2409
+ function projectNetworkCommercialEvents(input, sources) {
2410
+ const facts2 = [...input].sort(order);
2411
+ const exceptions2 = [];
2412
+ const unique = [];
2413
+ const digests = /* @__PURE__ */ new Map();
2414
+ for (const fact of facts2) {
2415
+ const prior = digests.get(fact.eventId);
2416
+ if (prior === void 0) {
2417
+ digests.set(fact.eventId, fact.digest);
2418
+ unique.push(fact);
2419
+ } else if (prior !== fact.digest) exceptions2.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2420
+ }
2421
+ const assignments = /* @__PURE__ */ new Map();
2422
+ const refs = /* @__PURE__ */ new Map();
2423
+ for (const fact of unique) {
2424
+ const direct = directSource(fact, sources);
2425
+ if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
2426
+ }
2427
+ let changed = true;
2428
+ while (changed) {
2429
+ changed = false;
2430
+ for (const fact of unique) {
2431
+ const assigned = assignments.get(fact.eventId);
2432
+ if (assigned) {
2433
+ const key = networkCommercialSourceKey(assigned);
2434
+ for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
2435
+ const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
2436
+ const before = keys2.size;
2437
+ keys2.add(key);
2438
+ refs.set(ref, keys2);
2439
+ if (keys2.size !== before) changed = true;
2440
+ }
2441
+ continue;
2442
+ }
2443
+ if (directSource(fact, sources) === "wrong") continue;
2444
+ const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
2445
+ if (keys.size !== 1) continue;
2446
+ const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
2447
+ if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
2448
+ assignments.set(fact.eventId, candidate);
2449
+ changed = true;
2450
+ }
2451
+ }
2452
+ }
2453
+ const latest = /* @__PURE__ */ new Map();
2454
+ for (const fact of unique) {
2455
+ if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
2456
+ exceptions2.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2457
+ continue;
2458
+ }
2459
+ const source = assignments.get(fact.eventId);
2460
+ if (!source) {
2461
+ const direct = directSource(fact, sources);
2462
+ const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
2463
+ exceptions2.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2464
+ continue;
2465
+ }
2466
+ const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
2467
+ const key = `${fact.objectKind}:${fact.objectId}`;
2468
+ const prior = latest.get(key);
2469
+ if (!prior || order(prior, fact) <= 0) latest.set(key, row);
2470
+ }
2471
+ const rows3 = [...latest.values()];
2472
+ const customers = rows3.filter((row) => row.objectKind === "customer");
2473
+ const subscriptions = rows3.filter((row) => row.objectKind === "subscription");
2474
+ const invoices = rows3.filter((row) => row.objectKind === "invoice");
2475
+ const refunds = rows3.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
2476
+ for (const charge of rows3.filter((row) => row.eventType === "charge.refunded")) {
2477
+ if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
2478
+ exceptions2.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
2479
+ }
2480
+ }
2481
+ const totalsMap = /* @__PURE__ */ new Map();
2482
+ const add = (row, field, amount) => {
2483
+ if (!row.currency) return;
2484
+ const key = `${row.sourceKey}:${row.currency}`;
2485
+ const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2486
+ total[field] += amount;
2487
+ total.netCents = total.paidCents - total.refundedCents;
2488
+ totalsMap.set(key, total);
2489
+ };
2490
+ for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
2491
+ for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
2492
+ return { customers, subscriptions, invoices, refunds, exceptions: exceptions2, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
2493
+ }
2494
+
2495
+ // src/network-commercial-reconcile.ts
2496
+ var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2497
+ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
2498
+ const objects = [];
2499
+ let startingAfter;
2500
+ for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
2501
+ const page = await client.request("GET", path, {
2502
+ ...params,
2503
+ limit: 100,
2504
+ ...startingAfter ? { starting_after: startingAfter } : {}
2505
+ });
2506
+ if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
2507
+ throw new Error("Stripe returned an invalid commercial list response");
2508
+ }
2509
+ const rows3 = page.data.map(asRaw).filter((row) => row !== null);
2510
+ if (rows3.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
2511
+ objects.push(...rows3);
2512
+ if (!page.has_more) return { objects, complete: true };
2513
+ const cursor = rows3.at(-1)?.id;
2514
+ if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
2515
+ startingAfter = cursor;
2516
+ }
2517
+ return { objects, complete: false };
2518
+ }
2519
+ function facts(snapshots) {
2520
+ const groups2 = [
2521
+ ["customer.updated", snapshots.customers.objects],
2522
+ ["customer.subscription.updated", snapshots.subscriptions.objects],
2523
+ ["invoice.updated", snapshots.invoices.objects],
2524
+ ["refund.updated", snapshots.refunds.objects]
2525
+ ];
2526
+ return groups2.flatMap(([eventType, objects]) => objects.map((object) => {
2527
+ const id6 = typeof object.id === "string" ? object.id : "invalid";
2528
+ return normalizeNetworkCommercialStripeEvent({
2529
+ id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id6}`,
2530
+ type: eventType,
2531
+ created: typeof object.created === "number" ? object.created : 0,
2532
+ livemode: object.livemode === true,
2533
+ data: { object }
2534
+ }, `stripe-snapshot:${eventType}:${id6}`);
2535
+ }));
2536
+ }
2537
+ var selected = (row) => ({
2538
+ status: row.status ?? null,
2539
+ currency: row.currency ?? null,
2540
+ amountCents: row.amountCents ?? null,
2541
+ amountPaidCents: row.amountPaidCents ?? null,
2542
+ amountRemainingCents: row.amountRemainingCents ?? null,
2543
+ customerId: row.customerId ?? null,
2544
+ subscriptionId: row.subscriptionId ?? null,
2545
+ invoiceId: row.invoiceId ?? null,
2546
+ priceId: row.priceId ?? null,
2547
+ quantity: row.quantity ?? null,
2548
+ applicationId: row.applicationId ?? null,
2549
+ personId: row.personId ?? null,
2550
+ membershipId: row.membershipId ?? null,
2551
+ currentPeriodStart: row.currentPeriodStart ?? null,
2552
+ currentPeriodEnd: row.currentPeriodEnd ?? null,
2553
+ cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
2554
+ });
2555
+ var index = (projection2) => new Map(
2556
+ [...projection2.customers, ...projection2.subscriptions, ...projection2.invoices, ...projection2.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
2557
+ );
2558
+ function reconcileStripeSnapshot(shadow, snapshots, sources) {
2559
+ const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
2560
+ const shadowRows = index(shadow);
2561
+ const stripeRows = index(stripe);
2562
+ const exceptions2 = [];
2563
+ const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
2564
+ if (!complete) exceptions2.push({ kind: "stripe_snapshot_truncated", detail: {} });
2565
+ for (const [key, row] of shadowRows) {
2566
+ const other = stripeRows.get(key);
2567
+ if (!other) {
2568
+ exceptions2.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
2569
+ } else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
2570
+ exceptions2.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
2571
+ }
2572
+ }
2573
+ for (const [key, row] of stripeRows) {
2574
+ if (!shadowRows.has(key)) exceptions2.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
2575
+ }
2576
+ if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
2577
+ exceptions2.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
2578
+ }
2579
+ return {
2580
+ ok: exceptions2.length === 0,
2581
+ complete,
2582
+ ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
2583
+ exceptions: exceptions2,
2584
+ stripeTotals: stripe.totals
2585
+ };
2586
+ }
2587
+
2588
+ // src/network-commercial-parity.ts
2589
+ var DIGEST = /^[0-9a-f]{64}$/;
2590
+ var MAX_RECORDS = 500;
2591
+ var text2 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
2592
+ function parityMessage(partition, kind, value) {
2593
+ return [
2594
+ "chapter-commercial-parity",
2595
+ "v1",
2596
+ partition.appId,
2597
+ partition.chapterId,
2598
+ partition.environment,
2599
+ partition.dataEnvironment,
2600
+ partition.runtime,
2601
+ kind,
2602
+ value
2603
+ ].join("");
2604
+ }
2605
+ async function parityKey(secret) {
2606
+ const encoder4 = new TextEncoder();
2607
+ return crypto.subtle.importKey(
2608
+ "raw",
2609
+ encoder4.encode(secret),
2610
+ { name: "HMAC", hash: "SHA-256" },
2611
+ false,
2612
+ ["sign"]
2613
+ );
2614
+ }
2615
+ async function digestWithKey(key, partition, kind, value) {
2616
+ const encoder4 = new TextEncoder();
2617
+ const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
2618
+ return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2619
+ }
2620
+ async function commercialParityDigest(secret, partition, kind, value) {
2621
+ return digestWithKey(await parityKey(secret), partition, kind, value);
2622
+ }
2623
+ async function buildChapterCommercialParitySnapshot(input) {
2624
+ const requestedMax = input.maxRecords ?? MAX_RECORDS;
2625
+ const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
2626
+ const eligible = input.rows.filter((row) => text2(row.id) && (text2(row.stripeCustomerId) || text2(row.stripeSubscriptionId)));
2627
+ const selected2 = eligible.slice(0, maxRecords);
2628
+ const key = await parityKey(input.secret);
2629
+ const records = await Promise.all(selected2.map(async (row) => {
2630
+ const applicationId = text2(row.id);
2631
+ const customerId = text2(row.stripeCustomerId);
2632
+ const subscriptionId = text2(row.stripeSubscriptionId);
2633
+ return {
2634
+ applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
2635
+ customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
2636
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
2637
+ status: text2(row.status, 64),
2638
+ canceled: row.canceled === true
2639
+ };
2640
+ }));
2641
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2642
+ const truncated = input.scanTruncated || eligible.length > maxRecords;
2643
+ return {
2644
+ version: 1,
2645
+ site: { id: input.siteId },
2646
+ partition: input.partition,
2647
+ generatedAt: input.now ?? Date.now(),
2648
+ complete: !truncated,
2649
+ truncated,
2650
+ counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
2651
+ records
2652
+ };
2653
+ }
2654
+ function recordKey(record4) {
2655
+ return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
2656
+ }
2657
+ function validSnapshot(snapshot, source) {
2658
+ const partition = snapshot?.partition;
2659
+ const records = snapshot?.records;
2660
+ if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
2661
+ const seen = /* @__PURE__ */ new Set();
2662
+ for (const record4 of records) {
2663
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text2(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
2664
+ const key = recordKey(record4);
2665
+ if (seen.has(key)) return false;
2666
+ seen.add(key);
2667
+ }
2668
+ return Number.isSafeInteger(snapshot.counts.scanned) && Number.isSafeInteger(snapshot.counts.eligible) && Number.isSafeInteger(snapshot.counts.returned) && snapshot.counts.returned === records.length && snapshot.counts.scanned >= snapshot.counts.eligible && snapshot.counts.eligible >= snapshot.counts.returned;
2669
+ }
2670
+ async function reconcileCommercialParitySnapshot(input) {
2671
+ if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
2672
+ const sourceKey = networkCommercialSourceKey(input.source);
2673
+ const key = await parityKey(input.secret);
2674
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text2(row.applicationId) && text2(row.objectId)).map(async (row) => {
2675
+ const customerId = text2(row.customerId);
2676
+ const subscriptionId = text2(row.subscriptionId) ?? text2(row.objectId);
2677
+ return {
2678
+ applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
2679
+ customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
2680
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
2681
+ status: null,
2682
+ canceled: false,
2683
+ stripeStatus: text2(row.status, 64),
2684
+ stripeCanceled: row.status === "canceled"
2685
+ };
2686
+ }));
2687
+ const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
2688
+ const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
2689
+ const exceptions2 = [];
2690
+ const records = [];
2691
+ for (const [key2, chapter] of chapterByKey) {
2692
+ const stripe = shadowByKey.get(key2);
2693
+ if (!stripe) {
2694
+ exceptions2.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
2695
+ continue;
2696
+ }
2697
+ const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
2698
+ records.push(compared);
2699
+ if (chapter.canceled !== stripe.stripeCanceled) {
2700
+ exceptions2.push({
2701
+ kind: "cancellation_mismatch",
2702
+ applicationDigest: chapter.applicationDigest,
2703
+ detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
2704
+ });
2705
+ }
2706
+ }
2707
+ for (const [key2, stripe] of shadowByKey) {
2708
+ if (!chapterByKey.has(key2)) {
2709
+ exceptions2.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
2710
+ }
2711
+ }
2712
+ if (!input.snapshot.complete || input.snapshot.truncated) {
2713
+ exceptions2.push({
2714
+ kind: "chapter_truncated",
2715
+ detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
2716
+ });
2717
+ }
2718
+ if (chapterByKey.size !== shadowByKey.size) {
2719
+ exceptions2.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
2720
+ }
2721
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2722
+ return {
2723
+ ok: input.snapshot.complete && exceptions2.length === 0,
2724
+ complete: input.snapshot.complete,
2725
+ counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
2726
+ records,
2727
+ exceptions: exceptions2
2728
+ };
2729
+ }
2730
+
2185
2731
  // src/leader.ts
2186
2732
  var import_crm3 = require("@odla-ai/crm");
2187
2733
  function leaderCrmConfig(options = {}) {
@@ -2350,14 +2896,14 @@ var import_crm4 = require("@odla-ai/crm");
2350
2896
 
2351
2897
  // src/tiers.ts
2352
2898
  function buildTierSeeds(groupId, configured = [], now = 0) {
2353
- return configured.map((tier, index) => ({
2899
+ return configured.map((tier, index2) => ({
2354
2900
  id: tier.id,
2355
2901
  groupId,
2356
2902
  name: tier.name,
2357
2903
  priceCents: tier.priceCents,
2358
2904
  ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
2359
2905
  ...tier.blurb ? { blurb: tier.blurb } : {},
2360
- sortOrder: tier.sortOrder ?? index,
2906
+ sortOrder: tier.sortOrder ?? index2,
2361
2907
  active: tier.active !== false,
2362
2908
  createdAt: now
2363
2909
  }));
@@ -2430,13 +2976,26 @@ function createChapterIntegration(chapter, options = {}) {
2430
2976
  const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
2431
2977
  const runbooks = isLeader ? {
2432
2978
  role: "leader",
2433
- slugs: ["chapter-leader", "chapter-network", "crm"],
2979
+ slugs: ["chapter-leader", "chapter-network", "crm", ...chapter.networkCommercialReadModel ? ["stripe-network-shadow"] : []],
2434
2980
  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
2981
  } : {
2436
2982
  role: "follower",
2437
2983
  slugs: ["chapter-follower", "chapter-network"],
2438
2984
  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
2985
  };
2986
+ const secrets = [];
2987
+ if (chapter.account !== "none") secrets.push({
2988
+ // Released Chapter workers request this effective name. odla-db maps it to
2989
+ // the canonical reserved `$clerk_secret` value written by the CLI.
2990
+ name: "clerk_secret_key",
2991
+ description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
2992
+ required: true
2993
+ });
2994
+ if (chapter.networkCommercialReadModel) secrets.push({
2995
+ name: "network_commercial_stripe_webhook_secret",
2996
+ description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
2997
+ required: true
2998
+ });
2440
2999
  return {
2441
3000
  id: "chapter",
2442
3001
  title: `Chapter: ${chapter.name}`,
@@ -2447,14 +3006,7 @@ function createChapterIntegration(chapter, options = {}) {
2447
3006
  },
2448
3007
  rules: { ...crmDesc.rules, ...chapter.rules },
2449
3008
  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
- }],
3009
+ secrets,
2458
3010
  probes: [
2459
3011
  ...crmDesc.probes ?? [],
2460
3012
  // The snapshot route is deliberately private. A credential-free smoke
@@ -2465,6 +3017,7 @@ function createChapterIntegration(chapter, options = {}) {
2465
3017
  path: "/api/network/shared-notes",
2466
3018
  expectedStatus: 401
2467
3019
  }] : [],
3020
+ ...chapter.network.readers.some((reader) => reader.commercialParity) ? [{ path: "/api/network/commercial-parity", expectedStatus: 401 }] : [],
2468
3021
  // Formation metadata is public only when the host explicitly enables
2469
3022
  // the bounded intake contract.
2470
3023
  ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
@@ -2477,7 +3030,11 @@ function createChapterIntegration(chapter, options = {}) {
2477
3030
  { path: "/api/admin/builder-library", expectedStatus: 401 },
2478
3031
  { path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
2479
3032
  ] : [],
2480
- ...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : []
3033
+ ...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : [],
3034
+ ...chapter.networkCommercialReadModel ? [
3035
+ { path: "/api/network-commercial/stripe-webhook", expectedStatus: 405 },
3036
+ { path: "/api/admin/network-commercial-read-model", expectedStatus: 401 }
3037
+ ] : []
2481
3038
  ],
2482
3039
  runbooks
2483
3040
  };
@@ -2485,7 +3042,7 @@ function createChapterIntegration(chapter, options = {}) {
2485
3042
 
2486
3043
  // src/member-content-validation-helpers.ts
2487
3044
  var ID = /^[a-z][a-z0-9-]{0,79}$/;
2488
- var SLUG7 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3045
+ var SLUG8 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2489
3046
  var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2490
3047
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2491
3048
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
@@ -2517,21 +3074,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2517
3074
  return valueText;
2518
3075
  }
2519
3076
  function offerValue(value, path, errors, tiers2, audience) {
2520
- const raw = record(value);
2521
- if (!raw) {
3077
+ const raw2 = record(value);
3078
+ if (!raw2) {
2522
3079
  errors.push(`${path} must be an object`);
2523
3080
  return void 0;
2524
3081
  }
2525
- unknownKeys(raw, OFFER_KEYS, path, errors);
2526
- const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
3082
+ unknownKeys(raw2, OFFER_KEYS, path, errors);
3083
+ const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2527
3084
  if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2528
3085
  if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2529
3086
  if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2530
3087
  errors.push(`${path}.tierId must grant access to this page`);
2531
3088
  }
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);
3089
+ const label = textValue(raw2.label, `${path}.label`, errors);
3090
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
3091
+ const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
2535
3092
  return {
2536
3093
  tierId,
2537
3094
  ...label ? { label } : {},
@@ -2583,37 +3140,37 @@ function validateMemberContentDocument(input, options = {}) {
2583
3140
  const ids = /* @__PURE__ */ new Set();
2584
3141
  const slugs = /* @__PURE__ */ new Set();
2585
3142
  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) {
3143
+ for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
3144
+ const path = `document.pages[${index2}]`;
3145
+ const raw2 = record(value);
3146
+ if (!raw2) {
2590
3147
  errors.push(`${path} must be an object`);
2591
3148
  continue;
2592
3149
  }
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}"`);
3150
+ unknownKeys(raw2, PAGE_KEYS, path, errors);
3151
+ const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
3152
+ const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
3153
+ const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
3154
+ if (id6 && !ID.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3155
+ if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
3156
+ if (ids.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
2600
3157
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2601
- ids.add(id5);
3158
+ ids.add(id6);
2602
3159
  slugs.add(slug);
2603
- const navRaw = record(raw.navigation);
2604
- let navigation = { section: "General", order: index };
3160
+ const navRaw = record(raw2.navigation);
3161
+ let navigation = { section: "General", order: index2 };
2605
3162
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
2606
3163
  else {
2607
3164
  unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2608
3165
  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) {
3166
+ const order2 = navRaw.order;
3167
+ if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
2611
3168
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2612
3169
  }
2613
3170
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2614
- navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
3171
+ navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
2615
3172
  }
2616
- const audienceRaw = record(raw.audience);
3173
+ const audienceRaw = record(raw2.audience);
2617
3174
  let audience = { kind: "all-members" };
2618
3175
  if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2619
3176
  else {
@@ -2635,12 +3192,12 @@ function validateMemberContentDocument(input, options = {}) {
2635
3192
  audience = { kind: "tiers", tierIds: normalized };
2636
3193
  } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2637
3194
  }
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) ?? "";
3195
+ const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
3196
+ const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
3197
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
2641
3198
  let upsell;
2642
- if (raw.upsell !== void 0) {
2643
- const upsellRaw = record(raw.upsell);
3199
+ if (raw2.upsell !== void 0) {
3200
+ const upsellRaw = record(raw2.upsell);
2644
3201
  if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2645
3202
  else {
2646
3203
  unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
@@ -2654,13 +3211,13 @@ function validateMemberContentDocument(input, options = {}) {
2654
3211
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2655
3212
  }
2656
3213
  }
2657
- pages.push({ id: id5, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
3214
+ pages.push({ id: id6, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2658
3215
  }
2659
3216
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2660
- for (const [index, page] of pages.entries()) {
3217
+ for (const [index2, page] of pages.entries()) {
2661
3218
  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`);
3219
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
3220
+ if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
2664
3221
  }
2665
3222
  for (const page of pages) {
2666
3223
  const seen = /* @__PURE__ */ new Set([page.id]);
@@ -2681,21 +3238,21 @@ function validateMemberContentDocument(input, options = {}) {
2681
3238
  const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2682
3239
  const seen = /* @__PURE__ */ new Set();
2683
3240
  upsellOverrides = [];
2684
- for (const [index, value] of values.entries()) {
2685
- const path = `document.upsellOverrides[${index}]`;
2686
- const raw = record(value);
2687
- if (!raw) {
3241
+ for (const [index2, value] of values.entries()) {
3242
+ const path = `document.upsellOverrides[${index2}]`;
3243
+ const raw2 = record(value);
3244
+ if (!raw2) {
2688
3245
  errors.push(`${path} must be an object`);
2689
3246
  continue;
2690
3247
  }
2691
- unknownKeys(raw, OVERRIDE_KEYS, path, errors);
2692
- const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
3248
+ unknownKeys(raw2, OVERRIDE_KEYS, path, errors);
3249
+ const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2693
3250
  if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2694
3251
  seen.add(pageId);
2695
3252
  const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2696
3253
  if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2697
3254
  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);
3255
+ const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2699
3256
  if (offer) upsellOverrides.push({ pageId, offer });
2700
3257
  }
2701
3258
  }
@@ -2820,7 +3377,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
2820
3377
  function unknownKeys2(value, allowed2, path, errors) {
2821
3378
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2822
3379
  }
2823
- function text(value, path, errors, max = MAX_TEXT2, required = true) {
3380
+ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
2824
3381
  if (value === void 0 && !required) return void 0;
2825
3382
  if (typeof value !== "string" || required && !value.trim()) {
2826
3383
  errors.push(`${path} must be a non-empty string`);
@@ -2830,23 +3387,23 @@ function text(value, path, errors, max = MAX_TEXT2, required = true) {
2830
3387
  if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
2831
3388
  return result;
2832
3389
  }
2833
- function integer(value, path, errors, minimum = 1) {
3390
+ function integer2(value, path, errors, minimum = 1) {
2834
3391
  if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
2835
3392
  return Number(value);
2836
3393
  }
2837
3394
  function scopeValue(value, path, errors) {
2838
- const raw = record2(value);
2839
- if (!raw) {
3395
+ const raw2 = record2(value);
3396
+ if (!raw2) {
2840
3397
  errors.push(`${path} must be an object`);
2841
3398
  return null;
2842
3399
  }
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 };
3400
+ unknownKeys2(raw2, SCOPE_KEYS, path, errors);
3401
+ if (raw2.kind === "global" || raw2.kind === "built_not_found") {
3402
+ if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
3403
+ return { kind: raw2.kind };
2847
3404
  }
2848
- if (raw.kind === "chapter") {
2849
- const chapterId = text(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3405
+ if (raw2.kind === "chapter") {
3406
+ const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
2850
3407
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
2851
3408
  return { kind: "chapter", chapterId };
2852
3409
  }
@@ -2854,28 +3411,28 @@ function scopeValue(value, path, errors) {
2854
3411
  return null;
2855
3412
  }
2856
3413
  function resourceValue(value, path, errors) {
2857
- const raw = record2(value);
2858
- if (!raw) {
3414
+ const raw2 = record2(value);
3415
+ if (!raw2) {
2859
3416
  errors.push(`${path} must be an object`);
2860
3417
  return null;
2861
3418
  }
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) ?? "";
3419
+ unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
3420
+ const id6 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
3421
+ if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
3422
+ const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
2866
3423
  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) ?? "";
3424
+ const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
3425
+ const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
3426
+ const status = text3(raw2.status, `${path}.status`, errors) ?? "";
2870
3427
  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`);
3428
+ const revision = integer2(raw2.revision, `${path}.revision`, errors);
3429
+ const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
3430
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
3431
+ if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
2875
3432
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
2876
3433
  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);
3434
+ const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
3435
+ const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
2879
3436
  if (url) {
2880
3437
  try {
2881
3438
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -2883,13 +3440,13 @@ function resourceValue(value, path, errors) {
2883
3440
  errors.push(`${path}.url must be a valid https URL`);
2884
3441
  }
2885
3442
  }
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`);
3443
+ const asset = raw2.brandAsset === void 0 ? null : record2(raw2.brandAsset);
3444
+ if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
2888
3445
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
2889
3446
  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) ?? "" }
3447
+ bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
3448
+ assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
3449
+ ...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
2893
3450
  } : void 0;
2894
3451
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
2895
3452
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -2897,59 +3454,59 @@ function resourceValue(value, path, errors) {
2897
3454
  if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
2898
3455
  if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
2899
3456
  if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
2900
- const provenanceRaw = record2(raw.provenance);
3457
+ const provenanceRaw = record2(raw2.provenance);
2901
3458
  if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
2902
3459
  if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
2903
3460
  const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
2904
3461
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
2905
3462
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
2906
3463
  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)
3464
+ chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
3465
+ resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
3466
+ resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
3467
+ documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
3468
+ promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3469
+ promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
2913
3470
  } : void 0;
2914
3471
  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),
3472
+ createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3473
+ createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
3474
+ updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3475
+ updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
2919
3476
  ...promotedFrom ? { promotedFrom } : {}
2920
3477
  };
2921
- return { id: id5, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
3478
+ return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
2922
3479
  }
2923
3480
  function validateBuilderLibraryDocument(value, options = {}) {
2924
3481
  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);
3482
+ const raw2 = record2(value);
3483
+ if (!raw2) return { ok: false, errors: ["document must be an object"] };
3484
+ unknownKeys2(raw2, ROOT_KEYS2, "document", errors);
3485
+ if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
3486
+ const revision = integer2(raw2.revision, "document.revision", errors);
3487
+ const generatedAt = integer2(raw2.generatedAt, "document.generatedAt", errors, 0);
3488
+ const sourceRaw = record2(raw2.source);
2932
3489
  if (!sourceRaw) errors.push("document.source must be an object");
2933
3490
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
2934
3491
  const sourceType = sourceRaw?.type;
2935
3492
  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) ?? "";
3493
+ const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
2937
3494
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
2938
3495
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2939
3496
  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");
3497
+ const targetRaw = raw2.target === void 0 ? null : record2(raw2.target);
3498
+ if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
2942
3499
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
2943
3500
  const target = targetRaw ? {
2944
- appId: text(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
2945
- environment: text(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3501
+ appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3502
+ environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
2946
3503
  } : void 0;
2947
3504
  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 : [];
3505
+ if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
3506
+ const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
2950
3507
  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);
3508
+ const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
3509
+ const ids = resources.map(({ id: id6 }) => id6);
2953
3510
  if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
2954
3511
  for (const resource of resources) {
2955
3512
  if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
@@ -3066,10 +3623,10 @@ function planDelivery(input) {
3066
3623
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
3067
3624
  const to = redirect ? input.group.debugEmail : input.to;
3068
3625
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
3069
- const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
3626
+ const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
3070
3627
 
3071
3628
  ` + render(tpl.text, vars) : render(tpl.text, vars);
3072
- return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
3629
+ return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
3073
3630
  }
3074
3631
 
3075
3632
  // src/notify.ts
@@ -3104,9 +3661,9 @@ async function sendTemplated(deps, input) {
3104
3661
  error = e instanceof Error ? e.message : String(e);
3105
3662
  }
3106
3663
  }
3107
- const id5 = deps.newId();
3664
+ const id6 = deps.newId();
3108
3665
  const row = {
3109
- id: id5,
3666
+ id: id6,
3110
3667
  groupId: input.group.id,
3111
3668
  to: decision.to,
3112
3669
  template: input.template,
@@ -3120,7 +3677,7 @@ async function sendTemplated(deps, input) {
3120
3677
  ...messageId ? { messageId } : {},
3121
3678
  ...error ? { error } : {}
3122
3679
  };
3123
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id5, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
3680
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
3124
3681
  return error ? { sent: false, reason: error } : { sent: true };
3125
3682
  }
3126
3683
  function emailGroupFrom(row) {
@@ -3306,29 +3863,29 @@ function validateAdmissionGrantDocument(input) {
3306
3863
  const grants = [];
3307
3864
  const seenIds = /* @__PURE__ */ new Set();
3308
3865
  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 : "";
3866
+ for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
3867
+ const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
3868
+ const id6 = typeof grant.id === "string" ? grant.id : "";
3312
3869
  const email = typeof grant.email === "string" ? grant.email.trim() : "";
3313
3870
  const normalized = normalizeAdmissionEmail(email);
3314
3871
  const decision = grant.decision;
3315
3872
  const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
3316
3873
  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`);
3874
+ if (!ID3.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
3875
+ if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
3876
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
3320
3877
  if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
3321
3878
  if (decision !== "waive_interview" && decision !== "require_interview") {
3322
- errors.push(`document.grants[${index}].decision is invalid`);
3879
+ errors.push(`document.grants[${index2}].decision is invalid`);
3323
3880
  }
3324
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
3881
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
3325
3882
  if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
3326
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
3883
+ errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
3327
3884
  }
3328
- seenIds.add(id5);
3885
+ seenIds.add(id6);
3329
3886
  seenEmails.add(normalized);
3330
3887
  grants.push({
3331
- id: id5,
3888
+ id: id6,
3332
3889
  email,
3333
3890
  decision,
3334
3891
  ...note ? { note } : {},
@@ -3399,8 +3956,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3399
3956
  const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
3400
3957
  const ops = [];
3401
3958
  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: {
3959
+ const id6 = String(row.id ?? "");
3960
+ if (id6 && !incomingIds.has(id6)) ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
3404
3961
  active: false,
3405
3962
  revokedAt: receivedAt,
3406
3963
  revokedBy: document.source.actor,
@@ -3409,10 +3966,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3409
3966
  } });
3410
3967
  }
3411
3968
  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,
3969
+ const id6 = `admission:parent:${document.source.appId}:${grant.id}`;
3970
+ const existing = current.find((row) => row.id === id6);
3971
+ ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
3972
+ id: id6,
3416
3973
  sourceGrantId: grant.id,
3417
3974
  sourceScope: "parent",
3418
3975
  sourceId: document.source.appId,
@@ -3433,7 +3990,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3433
3990
  ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
3434
3991
  ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
3435
3992
  ];
3436
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id5, attrs: retract });
3993
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id6, attrs: retract });
3437
3994
  }
3438
3995
  if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
3439
3996
  const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
@@ -3475,7 +4032,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
3475
4032
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3476
4033
  var CURRENCY = /^[a-z]{3}$/;
3477
4034
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
3478
- var text2 = (value, path, errors, max) => {
4035
+ var text4 = (value, path, errors, max) => {
3479
4036
  if (typeof value !== "string" || !value.trim()) {
3480
4037
  errors.push(`${path} is required`);
3481
4038
  return "";
@@ -3494,32 +4051,32 @@ function validateGiftOfferCatalog(input, tierIds) {
3494
4051
  if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
3495
4052
  const ids = /* @__PURE__ */ new Set();
3496
4053
  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);
4054
+ for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
4055
+ const path = `document.giftOffers.offers[${index2}]`;
4056
+ const item = record3(raw2);
3500
4057
  if (!item) {
3501
4058
  errors.push(`${path} must be an object`);
3502
4059
  continue;
3503
4060
  }
3504
4061
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
3505
4062
  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);
4063
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
4064
+ if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4065
+ if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
4066
+ ids.add(id6);
3510
4067
  const targetRaw = record3(item.target);
3511
4068
  let target = { type: "entitlement", entitlementKey: "invalid" };
3512
4069
  if (targetRaw?.type === "tier") {
3513
- const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
4070
+ const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3514
4071
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
3515
4072
  target = { type: "tier", tierId };
3516
4073
  } else if (targetRaw?.type === "entitlement") {
3517
- const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
4074
+ const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3518
4075
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
3519
4076
  target = { type: "entitlement", entitlementKey };
3520
4077
  } else errors.push(`${path}.target.type must be tier or entitlement`);
3521
4078
  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);
4079
+ const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3523
4080
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
3524
4081
  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
4082
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -3532,7 +4089,7 @@ function validateGiftOfferCatalog(input, tierIds) {
3532
4089
  if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
3533
4090
  if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
3534
4091
  offers.push({
3535
- id: id5,
4092
+ id: id6,
3536
4093
  target,
3537
4094
  billingMode: item.billingMode,
3538
4095
  stripePriceId,
@@ -3541,9 +4098,9 @@ function validateGiftOfferCatalog(input, tierIds) {
3541
4098
  redemptionLifetimeHours: lifetime,
3542
4099
  ...activeFrom !== void 0 ? { activeFrom } : {},
3543
4100
  ...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)
4101
+ title: text4(item.title, `${path}.title`, errors, 160),
4102
+ description: text4(item.description, `${path}.description`, errors, 2e3),
4103
+ ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3547
4104
  });
3548
4105
  }
3549
4106
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -3583,8 +4140,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
3583
4140
  const typeKey = type.toLowerCase();
3584
4141
  const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
3585
4142
  const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
3586
- const raw = `${prefix}:${hubRecordId}`;
3587
- if (readable && raw.length <= 64) return raw;
4143
+ const raw2 = `${prefix}:${hubRecordId}`;
4144
+ if (readable && raw2.length <= 64) return raw2;
3588
4145
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
3589
4146
  }
3590
4147
  function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
@@ -3854,14 +4411,14 @@ function periodSeries(points, now, count, periodMs) {
3854
4411
  for (const point of points) {
3855
4412
  if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
3856
4413
  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;
4414
+ const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
4415
+ current[index2] = (current[index2] ?? 0) + point.v;
3859
4416
  } 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;
4417
+ const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
4418
+ previous[index2] = (previous[index2] ?? 0) + point.v;
3862
4419
  }
3863
4420
  }
3864
- const labels = current.map((_, index) => new Date(start + index * periodMs).toISOString().slice(5, 10));
4421
+ const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
3865
4422
  return { labels, current, previous };
3866
4423
  }
3867
4424
  function dashboardMetricData(points, now) {
@@ -3945,9 +4502,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
3945
4502
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
3946
4503
  if (!found.ok) return false;
3947
4504
  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);
4505
+ const id6 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
4506
+ if (!id6) return false;
4507
+ return updateClerkUserMetadata(secretKey, id6, input, fetchImpl);
3951
4508
  }
3952
4509
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
3953
4510
  const { path, body } = clerkUserRequest(input);
@@ -4010,8 +4567,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
4010
4567
  const user = Array.isArray(data) ? data[0] : void 0;
4011
4568
  return user ? toRecord(user) : null;
4012
4569
  }
4013
- async function clerkGetUser(secretKey, id5, fetchImpl = fetch) {
4014
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id5)}`, secretKey, fetchImpl).catch(() => null);
4570
+ async function clerkGetUser(secretKey, id6, fetchImpl = fetch) {
4571
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id6)}`, secretKey, fetchImpl).catch(() => null);
4015
4572
  return data ? toRecord(data) : null;
4016
4573
  }
4017
4574
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -4027,8 +4584,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
4027
4584
  }
4028
4585
  return out;
4029
4586
  }
4030
- async function clerkSetRole(secretKey, id5, role, fetchImpl = fetch) {
4031
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id5)}/metadata`, {
4587
+ async function clerkSetRole(secretKey, id6, role, fetchImpl = fetch) {
4588
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id6)}/metadata`, {
4032
4589
  method: "PATCH",
4033
4590
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
4034
4591
  body: JSON.stringify({ public_metadata: { role } })
@@ -4254,8 +4811,8 @@ function chapterBrandFromTokens(compiled, base = {}) {
4254
4811
  function isReconcilable(meeting, now) {
4255
4812
  return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
4256
4813
  }
4257
- function reconcileMeetings(meetings2, events, now) {
4258
- const byEvent = new Map(events.map((e) => [e.eventId, e]));
4814
+ function reconcileMeetings(meetings2, events2, now) {
4815
+ const byEvent = new Map(events2.map((e) => [e.eventId, e]));
4259
4816
  const decisions = [];
4260
4817
  for (const m of meetings2) {
4261
4818
  if (!isReconcilable(m, now) || !m.googleEventId) continue;
@@ -4465,10 +5022,10 @@ function boundedString(value, path, errors, max, required = false) {
4465
5022
  errors.push(`${path} must be a string`);
4466
5023
  return void 0;
4467
5024
  }
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;
5025
+ const text6 = value.trim();
5026
+ if (required && !text6) errors.push(`${path} is required`);
5027
+ if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
5028
+ return text6;
4472
5029
  }
4473
5030
  function validateMembershipPageDocument(input, options = {}) {
4474
5031
  const errors = [];
@@ -4486,9 +5043,9 @@ function validateMembershipPageDocument(input, options = {}) {
4486
5043
  const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
4487
5044
  const seen = /* @__PURE__ */ new Set();
4488
5045
  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);
5046
+ for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
5047
+ const path = `document.tiers[${index2}]`;
5048
+ const item = objectRecord(raw2);
4492
5049
  if (!item) {
4493
5050
  errors.push(`${path} must be an object`);
4494
5051
  continue;
@@ -4517,8 +5074,8 @@ function validateMembershipPageDocument(input, options = {}) {
4517
5074
  else {
4518
5075
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
4519
5076
  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] : [];
5077
+ const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
5078
+ return text6 ? [text6] : [];
4522
5079
  });
4523
5080
  }
4524
5081
  }
@@ -4594,7 +5151,7 @@ function membershipPageDocument(input, options = {}) {
4594
5151
  }
4595
5152
 
4596
5153
  // src/signup-control-validate.ts
4597
- var SLUG8 = /^[a-z0-9][a-z0-9-]{1,62}$/;
5154
+ var SLUG9 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4598
5155
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
4599
5156
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
4600
5157
  var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
@@ -4617,7 +5174,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
4617
5174
  function unknownKeys3(value, allowed2, path, errors) {
4618
5175
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
4619
5176
  }
4620
- function text3(value, path, errors, max = 65536) {
5177
+ function text5(value, path, errors, max = 65536) {
4621
5178
  if (typeof value !== "string" || !value.trim()) {
4622
5179
  errors.push(`${path} is required`);
4623
5180
  return "";
@@ -4632,19 +5189,19 @@ function tiersOf(value, errors) {
4632
5189
  }
4633
5190
  if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
4634
5191
  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);
5192
+ return value.slice(0, 20).flatMap((raw2, index2) => {
5193
+ const path = `document.tiers[${index2}]`;
5194
+ const item = recordOf(raw2);
4638
5195
  if (!item) {
4639
5196
  errors.push(`${path} must be an object`);
4640
5197
  return [];
4641
5198
  }
4642
5199
  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);
5200
+ const id6 = text5(item.id, `${path}.id`, errors, 80);
5201
+ if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
5202
+ if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
5203
+ seen.add(id6);
5204
+ const name = text5(item.name, `${path}.name`, errors, 160);
4648
5205
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
4649
5206
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
4650
5207
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -4656,9 +5213,9 @@ function tiersOf(value, errors) {
4656
5213
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
4657
5214
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
4658
5215
  }
4659
- const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
5216
+ const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
4660
5217
  return [{
4661
- id: id5,
5218
+ id: id6,
4662
5219
  name,
4663
5220
  available: item.available === true,
4664
5221
  sortOrder: Number(item.sortOrder) || 0,
@@ -4712,7 +5269,7 @@ function policyOf(value, errors) {
4712
5269
  const result = {};
4713
5270
  for (const key of allowed2) {
4714
5271
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
4715
- result[key] = text3(input[key], `document.policy.${key}`, errors);
5272
+ result[key] = text5(input[key], `document.policy.${key}`, errors);
4716
5273
  }
4717
5274
  return result;
4718
5275
  }
@@ -4750,18 +5307,18 @@ function validateSignupControlDocument(input) {
4750
5307
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
4751
5308
  const source = recordOf(root.source);
4752
5309
  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");
5310
+ const appId = text5(source?.appId, "document.source.appId", errors, 63);
5311
+ const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
5312
+ const environment = text5(target?.environment, "document.target.environment", errors, 63);
5313
+ if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
5314
+ if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
4758
5315
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
4759
5316
  if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
4760
5317
  const tiers2 = tiersOf(root.tiers, errors);
4761
5318
  const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
4762
5319
  const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
4763
5320
  if (!page.ok) errors.push(...page.errors);
4764
- else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
5321
+ else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
4765
5322
  errors.push("document.membershipPage.tiers must follow the available tier display order");
4766
5323
  }
4767
5324
  const application = applicationOf(root.application, errors);
@@ -4796,7 +5353,7 @@ function validateSignupControlDocument(input) {
4796
5353
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
4797
5354
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
4798
5355
  var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
4799
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
5356
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
4800
5357
  var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
4801
5358
  var MAX_TOKENS = 256;
4802
5359
  var MAX_VALUE_LENGTH = 1e3;
@@ -4808,14 +5365,14 @@ function validatedTokens(input) {
4808
5365
  }
4809
5366
  const tokens = {};
4810
5367
  let serializedLength = 0;
4811
- for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
5368
+ for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
4812
5369
  if (!TOKEN_NAME.test(name)) {
4813
5370
  throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
4814
5371
  }
4815
- if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
5372
+ if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
4816
5373
  throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
4817
5374
  }
4818
- const value = String(raw).trim();
5375
+ const value = String(raw2).trim();
4819
5376
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
4820
5377
  throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
4821
5378
  }
@@ -4840,7 +5397,7 @@ function stateFor(revision, options) {
4840
5397
  metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
4841
5398
  };
4842
5399
  }
4843
- if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
5400
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
4844
5401
  throw new TypeError("signup-control theme revision metadata is invalid");
4845
5402
  }
4846
5403
  return {
@@ -4922,16 +5479,16 @@ function markdownSection(rawHeading) {
4922
5479
  }
4923
5480
  function markdownBenefits(lines, path, errors) {
4924
5481
  const benefits = [];
4925
- for (const [index, line] of lines.entries()) {
5482
+ for (const [index2, line] of lines.entries()) {
4926
5483
  if (!line.trim()) continue;
4927
5484
  const match = line.match(MARKDOWN_BENEFIT);
4928
5485
  if (!match) {
4929
- errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
5486
+ errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
4930
5487
  continue;
4931
5488
  }
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);
5489
+ const text6 = (match[1] ?? "").trim();
5490
+ if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
5491
+ if (text6) benefits.push(text6);
4935
5492
  }
4936
5493
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
4937
5494
  return benefits.slice(0, MAX_BENEFITS2);
@@ -4946,7 +5503,7 @@ function applyMembershipTierMarkdown(presentation, source) {
4946
5503
  const sections = /* @__PURE__ */ new Map();
4947
5504
  const prelude = [];
4948
5505
  let current = null;
4949
- for (const [index, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
5506
+ for (const [index2, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
4950
5507
  const heading = line.match(MARKDOWN_SECTION);
4951
5508
  if (heading) {
4952
5509
  const headingText = heading[1] ?? "";
@@ -4966,7 +5523,7 @@ function applyMembershipTierMarkdown(presentation, source) {
4966
5523
  continue;
4967
5524
  }
4968
5525
  if (MARKDOWN_HEADING.test(line)) {
4969
- errors.push(`membership Markdown line ${index + 1} uses an unsupported heading`);
5526
+ errors.push(`membership Markdown line ${index2 + 1} uses an unsupported heading`);
4970
5527
  continue;
4971
5528
  }
4972
5529
  if (current) sections.get(current)?.lines.push(line);
@@ -5031,7 +5588,7 @@ var exclusions = (chapterId, paidTierIds) => [
5031
5588
  id: chapterId,
5032
5589
  reason: "provider_identity",
5033
5590
  paths: [
5034
- ...paidTierIds.map((id5) => `tiers[${id5}].stripePriceId`),
5591
+ ...paidTierIds.map((id6) => `tiers[${id6}].stripePriceId`),
5035
5592
  "groups.stripePublishableKey",
5036
5593
  "applications.stripeCustomerId",
5037
5594
  "applications.stripeSubscriptionId",
@@ -5073,9 +5630,9 @@ async function exportChapterPromotion(input) {
5073
5630
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
5074
5631
  receiptDigests: [input.digest]
5075
5632
  };
5076
- const artifact = (kind, id5, content) => ({
5633
+ const artifact = (kind, id6, content) => ({
5077
5634
  kind,
5078
- id: id5,
5635
+ id: id6,
5079
5636
  sourceRevision: revision,
5080
5637
  provenance: promotionProvenance,
5081
5638
  content: (0, import_promotion.canonicalPromotionValue)(content)
@@ -5114,7 +5671,7 @@ async function exportChapterPromotion(input) {
5114
5671
  }
5115
5672
 
5116
5673
  // src/member-system-authority.ts
5117
- var rows2 = (group) => group.fields.map((id5) => ({ ...group, id: id5 }));
5674
+ var rows2 = (group) => group.fields.map((id6) => ({ ...group, id: id6 }));
5118
5675
  var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
5119
5676
  ...rows2({
5120
5677
  scope: "network",
@@ -5499,10 +6056,10 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
5499
6056
  if (supplied.length === keys.length) return "apply";
5500
6057
  return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
5501
6058
  }
5502
- function planMemberSystemEventReplay(events) {
6059
+ function planMemberSystemEventReplay(events2) {
5503
6060
  const seen = /* @__PURE__ */ new Map();
5504
6061
  const plan = { accepted: [], replayed: [], collisions: [] };
5505
- for (const event of events) {
6062
+ for (const event of events2) {
5506
6063
  const digest = seen.get(event.id);
5507
6064
  if (digest === void 0) {
5508
6065
  seen.set(event.id, event.digest);