@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.js CHANGED
@@ -36,7 +36,7 @@ function resolveApplication(a) {
36
36
  throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
37
37
  }
38
38
  }
39
- const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
39
+ const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
40
40
  if (duplicate) {
41
41
  throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
42
42
  }
@@ -115,8 +115,8 @@ async function submitApplication(db, chapter, fields, opts) {
115
115
  if (app.requireDisclaimerAck && !acked) {
116
116
  return { ok: false, error: "disclaimerAck is required" };
117
117
  }
118
- const id5 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
119
- const row = { id: id5, status: chapter.pipeline.initial, createdAt: opts.now };
118
+ const id6 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
119
+ const row = { id: id6, status: chapter.pipeline.initial, createdAt: opts.now };
120
120
  for (const f of [...app.required, ...app.optional]) {
121
121
  if (fieldStates[f]?.visible === false) continue;
122
122
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
@@ -140,19 +140,19 @@ async function submitApplication(db, chapter, fields, opts) {
140
140
  }
141
141
  if (acked) row.disclaimerAckAt = opts.now;
142
142
  const ops = [
143
- { t: "update", ns: "applications", id: id5, attrs: row }
143
+ { t: "update", ns: "applications", id: id6, attrs: row }
144
144
  ];
145
145
  if (opts.admissionGrant?.decision === "waive_interview") {
146
146
  ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
147
147
  consumedAt: opts.now,
148
- consumedApplicationId: id5
148
+ consumedApplicationId: id6
149
149
  } });
150
150
  }
151
151
  const { duplicate } = await db.transact(
152
152
  ops,
153
153
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
154
154
  );
155
- return { ok: true, id: id5, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
155
+ return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
156
156
  }
157
157
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
158
158
  return {
@@ -548,6 +548,64 @@ var networkPersonEntities = {
548
548
  networkPersonEvents: networkPersonEventsEntity
549
549
  };
550
550
 
551
+ // src/schema-network-commercial.ts
552
+ var string2 = (flags = {}) => ({
553
+ type: "string",
554
+ unique: flags.unique ?? false,
555
+ indexed: flags.indexed ?? false,
556
+ optional: flags.optional ?? false
557
+ });
558
+ var number2 = (flags = {}) => ({
559
+ type: "number",
560
+ unique: false,
561
+ indexed: flags.indexed ?? false,
562
+ optional: flags.optional ?? false
563
+ });
564
+ var boolean = (flags = {}) => ({
565
+ type: "boolean",
566
+ unique: false,
567
+ indexed: flags.indexed ?? false,
568
+ optional: false
569
+ });
570
+ var json2 = () => ({ type: "json", unique: false, indexed: false, optional: false });
571
+ var id4 = () => string2({ unique: true, indexed: true });
572
+ var events = { attrs: {
573
+ id: id4(),
574
+ eventId: string2({ unique: true, indexed: true }),
575
+ digest: string2({ indexed: true }),
576
+ eventType: string2({ indexed: true }),
577
+ eventCreated: number2({ indexed: true }),
578
+ livemode: boolean({ indexed: true }),
579
+ fact: json2(),
580
+ receivedAt: number2({ indexed: true })
581
+ } };
582
+ var projection = () => ({ attrs: {
583
+ id: id4(),
584
+ providerId: string2({ indexed: true }),
585
+ sourceKey: string2({ indexed: true }),
586
+ status: string2({ indexed: true }),
587
+ snapshot: json2(),
588
+ eventCreated: number2({ indexed: true }),
589
+ updatedAt: number2({ indexed: true })
590
+ } });
591
+ var exceptions = { attrs: {
592
+ id: id4(),
593
+ eventId: string2({ indexed: true }),
594
+ kind: string2({ indexed: true }),
595
+ sourceKey: string2({ indexed: true, optional: true }),
596
+ status: string2({ indexed: true }),
597
+ detail: json2(),
598
+ updatedAt: number2({ indexed: true })
599
+ } };
600
+ var networkCommercialEntities = {
601
+ networkCommercialEvents: events,
602
+ networkCommercialCustomers: projection(),
603
+ networkCommercialSubscriptions: projection(),
604
+ networkCommercialInvoices: projection(),
605
+ networkCommercialRefunds: projection(),
606
+ networkCommercialExceptions: exceptions
607
+ };
608
+
551
609
  // src/schema.ts
552
610
  function attr5(type, flags = {}) {
553
611
  return {
@@ -557,10 +615,10 @@ function attr5(type, flags = {}) {
557
615
  optional: flags.optional ?? false
558
616
  };
559
617
  }
560
- var id4 = () => attr5("string", { unique: true, indexed: true });
618
+ var id5 = () => attr5("string", { unique: true, indexed: true });
561
619
  var admins = {
562
620
  attrs: {
563
- id: id4(),
621
+ id: id5(),
564
622
  email: attr5("string", { unique: true, indexed: true }),
565
623
  name: attr5("string", { optional: true }),
566
624
  note: attr5("string", { optional: true })
@@ -568,7 +626,7 @@ var admins = {
568
626
  };
569
627
  var superAdmins = {
570
628
  attrs: {
571
- id: id4(),
629
+ id: id5(),
572
630
  email: attr5("string", { unique: true, indexed: true }),
573
631
  note: attr5("string", { optional: true }),
574
632
  createdAt: attr5("number", { indexed: true })
@@ -576,7 +634,7 @@ var superAdmins = {
576
634
  };
577
635
  var networkNotes = {
578
636
  attrs: {
579
- id: id4(),
637
+ id: id5(),
580
638
  recordKey: attr5("string", { indexed: true }),
581
639
  targetId: attr5("string", { indexed: true }),
582
640
  recordType: attr5("string", { indexed: true }),
@@ -589,7 +647,7 @@ var networkNotes = {
589
647
  };
590
648
  var signupControls = {
591
649
  attrs: {
592
- id: id4(),
650
+ id: id5(),
593
651
  sourceId: attr5("string", { indexed: true }),
594
652
  targetId: attr5("string", { indexed: true }),
595
653
  environment: attr5("string", { indexed: true }),
@@ -602,7 +660,7 @@ var signupControls = {
602
660
  };
603
661
  var memberContentControls = {
604
662
  attrs: {
605
- id: id4(),
663
+ id: id5(),
606
664
  sourceId: attr5("string", { indexed: true }),
607
665
  targetId: attr5("string", { indexed: true }),
608
666
  environment: attr5("string", { indexed: true }),
@@ -615,7 +673,7 @@ var memberContentControls = {
615
673
  };
616
674
  var memberContentLocal = {
617
675
  attrs: {
618
- id: id4(),
676
+ id: id5(),
619
677
  chapterId: attr5("string", { indexed: true }),
620
678
  environment: attr5("string", { indexed: true }),
621
679
  revision: attr5("number", { indexed: true }),
@@ -626,7 +684,7 @@ var memberContentLocal = {
626
684
  };
627
685
  var builderLibraryControls = {
628
686
  attrs: {
629
- id: id4(),
687
+ id: id5(),
630
688
  sourceId: attr5("string", { indexed: true }),
631
689
  targetId: attr5("string", { indexed: true }),
632
690
  environment: attr5("string", { indexed: true }),
@@ -639,7 +697,7 @@ var builderLibraryControls = {
639
697
  };
640
698
  var builderLibraryLocal = {
641
699
  attrs: {
642
- id: id4(),
700
+ id: id5(),
643
701
  appId: attr5("string", { indexed: true }),
644
702
  environment: attr5("string", { indexed: true }),
645
703
  revision: attr5("number", { indexed: true }),
@@ -650,7 +708,7 @@ var builderLibraryLocal = {
650
708
  };
651
709
  var groups = {
652
710
  attrs: {
653
- id: id4(),
711
+ id: id5(),
654
712
  name: attr5("string"),
655
713
  standardPriceCents: attr5("number"),
656
714
  foundingDiscountCents: attr5("number"),
@@ -673,7 +731,7 @@ var groups = {
673
731
  };
674
732
  var tiers = {
675
733
  attrs: {
676
- id: id4(),
734
+ id: id5(),
677
735
  groupId: attr5("string", { indexed: true }),
678
736
  name: attr5("string"),
679
737
  priceCents: attr5("number"),
@@ -686,7 +744,7 @@ var tiers = {
686
744
  };
687
745
  var meetings = {
688
746
  attrs: {
689
- id: id4(),
747
+ id: id5(),
690
748
  applicationId: attr5("string", { indexed: true }),
691
749
  groupId: attr5("string", { indexed: true }),
692
750
  startAt: attr5("number", { indexed: true }),
@@ -705,7 +763,7 @@ var meetings = {
705
763
  };
706
764
  var emailLog = {
707
765
  attrs: {
708
- id: id4(),
766
+ id: id5(),
709
767
  groupId: attr5("string", { indexed: true }),
710
768
  applicationId: attr5("string", { indexed: true, optional: true }),
711
769
  to: attr5("string", { indexed: true }),
@@ -720,7 +778,7 @@ var emailLog = {
720
778
  sentAt: attr5("number", { indexed: true })
721
779
  }
722
780
  };
723
- function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false) {
781
+ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false, includeNetworkCommercialReadModel = false) {
724
782
  const entities = {};
725
783
  if (mode === "chapter") {
726
784
  entities.applications = applicationsFor(application);
@@ -757,6 +815,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
757
815
  if (includeNetworkPersonRegistry) {
758
816
  Object.assign(entities, networkPersonEntities);
759
817
  }
818
+ if (includeNetworkCommercialReadModel) Object.assign(entities, networkCommercialEntities);
760
819
  const schema = { entities, links: {} };
761
820
  const rules = {};
762
821
  for (const ns of Object.keys(entities)) {
@@ -928,8 +987,8 @@ function resolveAuth(mode, auth) {
928
987
  return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
929
988
  }
930
989
  function roleFromClaim(payload, auth) {
931
- const raw = payload[auth.claim];
932
- return typeof raw === "string" && auth.ladder.includes(raw) ? raw : auth.ladder[0];
990
+ const raw2 = payload[auth.claim];
991
+ return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
933
992
  }
934
993
  function isAdminRole(role, auth) {
935
994
  return role === auth.adminRole;
@@ -1408,9 +1467,9 @@ var APPLICATION_LABELS = {
1408
1467
  phone: "Phone",
1409
1468
  state: "State"
1410
1469
  };
1411
- function humanize(id5) {
1412
- const text4 = id5.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1413
- return text4 ? `${text4[0].toUpperCase()}${text4.slice(1)}` : id5;
1470
+ function humanize(id6) {
1471
+ const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1472
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
1414
1473
  }
1415
1474
  function applicationDefs(chapter) {
1416
1475
  try {
@@ -1419,23 +1478,23 @@ function applicationDefs(chapter) {
1419
1478
  return {};
1420
1479
  }
1421
1480
  }
1422
- function applicationType(id5, def) {
1481
+ function applicationType(id6, def) {
1423
1482
  if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
1424
- return id5 === "email" ? "email" : id5 === "linkedin" ? "url" : "string";
1483
+ return id6 === "email" ? "email" : id6 === "linkedin" ? "url" : "string";
1425
1484
  }
1426
1485
  function applicationFlowFields(chapter, application = chapter.application) {
1427
1486
  const defs = applicationDefs(chapter);
1428
- return [...application.required, ...application.optional].map((id5, order) => {
1429
- const def = defs[id5];
1430
- const conditions = application.conditions[id5];
1431
- const type = applicationType(id5, def);
1487
+ return [...application.required, ...application.optional].map((id6, order2) => {
1488
+ const def = defs[id6];
1489
+ const conditions = application.conditions[id6];
1490
+ const type = applicationType(id6, def);
1432
1491
  return {
1433
- id: id5,
1434
- label: def?.label ?? APPLICATION_LABELS[id5] ?? humanize(id5),
1492
+ id: id6,
1493
+ label: def?.label ?? APPLICATION_LABELS[id6] ?? humanize(id6),
1435
1494
  type,
1436
- required: application.required.includes(id5),
1437
- order,
1438
- maxLength: application.maxLen[id5] ?? application.defaultMaxLen,
1495
+ required: application.required.includes(id6),
1496
+ order: order2,
1497
+ maxLength: application.maxLen[id6] ?? application.defaultMaxLen,
1439
1498
  ...type === "enum" && def?.options ? { options: [...def.options] } : {},
1440
1499
  ...conditions ? { conditions: { ...conditions } } : {},
1441
1500
  ...type === "email" ? { emailIdentity: "literal" } : {}
@@ -1445,17 +1504,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
1445
1504
  function resolveFormationFlowFields(crm, formation) {
1446
1505
  if (!formation.enabled) return [];
1447
1506
  const def = crm.type(formation.type);
1448
- return [...formation.required, ...formation.optional].map((id5, order) => {
1449
- const field = def.fields[id5];
1507
+ return [...formation.required, ...formation.optional].map((id6, order2) => {
1508
+ const field = def.fields[id6];
1450
1509
  return {
1451
- id: id5,
1452
- label: field.label ?? humanize(id5),
1510
+ id: id6,
1511
+ label: field.label ?? humanize(id6),
1453
1512
  type: field.type,
1454
- required: formation.required.includes(id5),
1455
- order,
1456
- maxLength: formation.maxLen[id5] ?? formation.defaultMaxLen,
1513
+ required: formation.required.includes(id6),
1514
+ order: order2,
1515
+ maxLength: formation.maxLen[id6] ?? formation.defaultMaxLen,
1457
1516
  ...field.options ? { options: [...field.options] } : {},
1458
- ...id5 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1517
+ ...id6 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1459
1518
  };
1460
1519
  });
1461
1520
  }
@@ -1495,7 +1554,7 @@ function resolveLeaderFormation(input, crm) {
1495
1554
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1496
1555
  }
1497
1556
  }
1498
- const duplicates = [...required, ...optional].filter((field, index, fields) => fields.indexOf(field) !== index);
1557
+ const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
1499
1558
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1500
1559
  const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
1501
1560
  if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
@@ -1546,7 +1605,7 @@ var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
1546
1605
  var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
1547
1606
  var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
1548
1607
  function validateSharedNotes(opts) {
1549
- const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1608
+ const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
1550
1609
  const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type] || (opts.fields ? !opts.fields[type] : false));
1551
1610
  if (duplicate || invalid) {
1552
1611
  throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
@@ -1556,7 +1615,7 @@ function validateEditableFields(opts) {
1556
1615
  for (const [type, names] of Object.entries(opts.values)) {
1557
1616
  const readable = opts.readable?.[type];
1558
1617
  const def = opts.crm.config.types[type];
1559
- const duplicate = names.some((name, index) => names.indexOf(name) !== index);
1618
+ const duplicate = names.some((name, index2) => names.indexOf(name) !== index2);
1560
1619
  const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
1561
1620
  const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
1562
1621
  if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
@@ -1567,7 +1626,7 @@ function validateEditableFields(opts) {
1567
1626
  }
1568
1627
  }
1569
1628
  function validateStageTransitions(opts) {
1570
- const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
1629
+ const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
1571
1630
  const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
1572
1631
  if (duplicate || invalid) {
1573
1632
  throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
@@ -1668,7 +1727,8 @@ function resolveNetwork(config, crm) {
1668
1727
  sharedNotes,
1669
1728
  editableFields,
1670
1729
  stageTransitions,
1671
- ...target.admissionGrants === true ? { admissionGrants: true } : {}
1730
+ ...target.admissionGrants === true ? { admissionGrants: true } : {},
1731
+ ...target.commercialParity === true ? { commercialParity: true } : {}
1672
1732
  });
1673
1733
  }
1674
1734
  const readerIds = /* @__PURE__ */ new Set();
@@ -1683,9 +1743,9 @@ function resolveNetwork(config, crm) {
1683
1743
  readerIds.add(reader.id);
1684
1744
  const readerFields = reader.fields ?? {};
1685
1745
  const entries = Object.entries(readerFields);
1686
- if (entries.length === 0 && !reader.admissionGrants) {
1746
+ if (entries.length === 0 && !reader.admissionGrants && !reader.commercialParity) {
1687
1747
  throw new Error(
1688
- `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
1748
+ `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only or commercial-parity reader`
1689
1749
  );
1690
1750
  }
1691
1751
  for (const [type, names] of entries) {
@@ -1743,7 +1803,8 @@ function resolveNetwork(config, crm) {
1743
1803
  sharedNotes,
1744
1804
  editableFields,
1745
1805
  stageTransitions,
1746
- ...reader.admissionGrants === true ? { admissionGrants: true } : {}
1806
+ ...reader.admissionGrants === true ? { admissionGrants: true } : {},
1807
+ ...reader.commercialParity === true ? { commercialParity: true } : {}
1747
1808
  });
1748
1809
  }
1749
1810
  return { targets, readers };
@@ -1757,22 +1818,50 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
1757
1818
  if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
1758
1819
  const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
1759
1820
  if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
1760
- if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id5) => !SLUG2.test(id5))) {
1821
+ if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id6) => !SLUG2.test(id6))) {
1761
1822
  throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
1762
1823
  }
1763
- const unknown = chapterIds.filter((id5) => !targetIds.includes(id5));
1824
+ const unknown = chapterIds.filter((id6) => !targetIds.includes(id6));
1764
1825
  if (unknown.length) {
1765
- throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id5) => `"${id5}"`).join(", ")} are not configured network targets`);
1826
+ throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id6) => `"${id6}"`).join(", ")} are not configured network targets`);
1766
1827
  }
1767
1828
  return { mode: "shadow", chapterIds };
1768
1829
  }
1769
1830
 
1831
+ // src/config-network-commercial.ts
1832
+ var SLUG3 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
1833
+ var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
1834
+ function networkCommercialSourceKey(source) {
1835
+ return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
1836
+ }
1837
+ function resolveNetworkCommercialReadModel(mode, input, targetIds) {
1838
+ if (!input) return null;
1839
+ if (mode !== "hub") throw new Error("defineChapter.networkCommercialReadModel: only hub mode may own the network commercial projection");
1840
+ if (input.mode !== "shadow") throw new Error('defineChapter.networkCommercialReadModel.mode: must be "shadow"');
1841
+ if (!Array.isArray(input.sources) || input.sources.length === 0) {
1842
+ throw new Error("defineChapter.networkCommercialReadModel.sources: at least one exact source is required");
1843
+ }
1844
+ const sources = input.sources.map((source) => ({ ...source }));
1845
+ for (const source of sources) {
1846
+ 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") {
1847
+ throw new Error("defineChapter.networkCommercialReadModel.sources: every source requires valid app, chapter, environment, data-environment, runtime, and Stripe mode values");
1848
+ }
1849
+ if (!targetIds.includes(source.chapterId)) {
1850
+ throw new Error(`defineChapter.networkCommercialReadModel.sources: chapter "${source.chapterId}" is not a configured network target`);
1851
+ }
1852
+ }
1853
+ if (new Set(sources.map(networkCommercialSourceKey)).size !== sources.length) {
1854
+ throw new Error("defineChapter.networkCommercialReadModel.sources: exact source partitions must be distinct");
1855
+ }
1856
+ return { mode: "shadow", sources };
1857
+ }
1858
+
1770
1859
  // src/config-signup-control.ts
1771
- var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1860
+ var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1772
1861
  var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
1773
1862
  function resolveSignupControl(input) {
1774
1863
  if (!input) return null;
1775
- if (!SLUG3.test(input.sourceId)) {
1864
+ if (!SLUG4.test(input.sourceId)) {
1776
1865
  throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
1777
1866
  }
1778
1867
  if (input.stripeMode !== "test" && input.stripeMode !== "live") {
@@ -1786,12 +1875,12 @@ function resolveSignupControl(input) {
1786
1875
  }
1787
1876
 
1788
1877
  // src/config-member-content.ts
1789
- var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1878
+ var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1790
1879
  var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
1791
1880
  function resolveMemberContent(input) {
1792
1881
  if (!input) return null;
1793
1882
  const parent = input.parent;
1794
- if (parent && !SLUG4.test(parent.sourceId)) {
1883
+ if (parent && !SLUG5.test(parent.sourceId)) {
1795
1884
  throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
1796
1885
  }
1797
1886
  const secretName = parent?.secretName?.trim() || "member_content_control_secret";
@@ -1806,22 +1895,22 @@ function resolveMemberContent(input) {
1806
1895
  }
1807
1896
 
1808
1897
  // src/config-builder-library.ts
1809
- var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1898
+ var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1810
1899
  function resolveBuilderLibrary(mode, input, network = { targets: [], readers: [] }) {
1811
1900
  if (!input) return null;
1812
1901
  if (input.sources !== void 0 && !Array.isArray(input.sources)) {
1813
1902
  throw new Error("defineChapter.builderLibrary.sources: must be an array");
1814
1903
  }
1815
1904
  const seen = /* @__PURE__ */ new Set();
1816
- const sources = (input.sources ?? []).map((source, index) => {
1817
- if (!source || typeof source !== "object" || !SLUG5.test(source.sourceId)) {
1818
- throw new Error(`defineChapter.builderLibrary.sources[${index}].sourceId: must be a lowercase app slug`);
1905
+ const sources = (input.sources ?? []).map((source, index2) => {
1906
+ if (!source || typeof source !== "object" || !SLUG6.test(source.sourceId)) {
1907
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}].sourceId: must be a lowercase app slug`);
1819
1908
  }
1820
1909
  if (source.sourceId === "built-not-found" && mode === "hub") {
1821
- throw new Error(`defineChapter.builderLibrary.sources[${index}]: a hub accepts Chapter sources, not itself`);
1910
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}]: a hub accepts Chapter sources, not itself`);
1822
1911
  }
1823
1912
  if (mode === "chapter" && source.sourceId !== "built-not-found") {
1824
- throw new Error(`defineChapter.builderLibrary.sources[${index}]: Chapters may accept only the built-not-found source`);
1913
+ throw new Error(`defineChapter.builderLibrary.sources[${index2}]: Chapters may accept only the built-not-found source`);
1825
1914
  }
1826
1915
  if (seen.has(source.sourceId)) {
1827
1916
  throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
@@ -1829,14 +1918,14 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
1829
1918
  seen.add(source.sourceId);
1830
1919
  if ("secretName" in source) {
1831
1920
  throw new Error(
1832
- `defineChapter.builderLibrary.sources[${index}].secretName: remove this field; Builder Library reuses the Chapter network edge`
1921
+ `defineChapter.builderLibrary.sources[${index2}].secretName: remove this field; Builder Library reuses the Chapter network edge`
1833
1922
  );
1834
1923
  }
1835
1924
  const networkPeer = mode === "hub" ? network.targets.some((target) => target.id === source.sourceId) : network.readers.some((reader) => reader.id === source.sourceId);
1836
1925
  if (!networkPeer) {
1837
1926
  const side = mode === "hub" ? "targets" : "readers";
1838
1927
  throw new Error(
1839
- `defineChapter.builderLibrary.sources[${index}]: "${source.sourceId}" must already be configured in network.${side}`
1928
+ `defineChapter.builderLibrary.sources[${index2}]: "${source.sourceId}" must already be configured in network.${side}`
1840
1929
  );
1841
1930
  }
1842
1931
  return { sourceId: source.sourceId };
@@ -1845,15 +1934,15 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
1845
1934
  }
1846
1935
 
1847
1936
  // src/config.ts
1848
- var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1937
+ var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1849
1938
  function isResolvedCrm(x) {
1850
1939
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
1851
1940
  }
1852
1941
  function defineChapter(config) {
1853
1942
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1854
- const { id: id5, name } = config;
1855
- if (typeof id5 !== "string" || !SLUG6.test(id5)) {
1856
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id5)}`);
1943
+ const { id: id6, name } = config;
1944
+ if (typeof id6 !== "string" || !SLUG7.test(id6)) {
1945
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id6)}`);
1857
1946
  }
1858
1947
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
1859
1948
  const mode = config.mode ?? "chapter";
@@ -1881,6 +1970,11 @@ function defineChapter(config) {
1881
1970
  config.networkPersonRegistry,
1882
1971
  network.targets.map((target) => target.id)
1883
1972
  );
1973
+ const networkCommercialReadModel = resolveNetworkCommercialReadModel(
1974
+ mode,
1975
+ config.networkCommercialReadModel,
1976
+ network.targets.map((target) => target.id)
1977
+ );
1884
1978
  const signupControl = resolveSignupControl(config.signupControl);
1885
1979
  const memberContent = resolveMemberContent(config.memberContent);
1886
1980
  const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary, network);
@@ -1900,7 +1994,8 @@ function defineChapter(config) {
1900
1994
  Boolean(memberContent),
1901
1995
  Boolean(builderLibrary),
1902
1996
  network.targets.some((target) => target.admissionGrants),
1903
- Boolean(networkPersonRegistry)
1997
+ Boolean(networkPersonRegistry),
1998
+ Boolean(networkCommercialReadModel)
1904
1999
  );
1905
2000
  const services = config.services ?? ["db", "calendar", "o11y"];
1906
2001
  const account = config.account ?? "none";
@@ -1950,7 +2045,7 @@ function defineChapter(config) {
1950
2045
  };
1951
2046
  const chapter = {
1952
2047
  config,
1953
- id: id5,
2048
+ id: id6,
1954
2049
  name,
1955
2050
  mode,
1956
2051
  brand,
@@ -1960,6 +2055,7 @@ function defineChapter(config) {
1960
2055
  memberContent,
1961
2056
  builderLibrary,
1962
2057
  networkPersonRegistry,
2058
+ networkCommercialReadModel,
1963
2059
  formation,
1964
2060
  crm,
1965
2061
  auth,
@@ -1977,6 +2073,447 @@ function defineChapter(config) {
1977
2073
  return chapter;
1978
2074
  }
1979
2075
 
2076
+ // src/network-commercial.ts
2077
+ var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2078
+ var text = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
2079
+ var integer = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
2080
+ var bool = (value) => typeof value === "boolean" ? value : void 0;
2081
+ var providerId = (value, prefix) => {
2082
+ const valueText = text(value);
2083
+ return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
2084
+ };
2085
+ function metadataCandidates(object) {
2086
+ const parent = raw(object.parent);
2087
+ const subscriptionDetails = raw(object.subscription_details);
2088
+ const parentSubscription = raw(parent.subscription_details);
2089
+ return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
2090
+ }
2091
+ function metadataValue(candidates, key) {
2092
+ return candidates.map((candidate) => text(candidate[key])).find(Boolean);
2093
+ }
2094
+ function refId(value, prefix) {
2095
+ if (typeof value === "string") return providerId(value, prefix);
2096
+ return providerId(raw(value).id, prefix);
2097
+ }
2098
+ function subscriptionRef(object) {
2099
+ return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
2100
+ }
2101
+ function firstPrice(object) {
2102
+ const data = raw(object.items).data;
2103
+ const item = Array.isArray(data) ? raw(data[0]) : {};
2104
+ const priceId = refId(item.price, "price_");
2105
+ const quantity = integer(item.quantity);
2106
+ return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
2107
+ }
2108
+ function normalizeNetworkCommercialStripeEvent(input, digest) {
2109
+ const event = raw(input);
2110
+ const eventId = providerId(event.id, "evt_") ?? "";
2111
+ const eventType = text(event.type) ?? "";
2112
+ const object = raw(raw(event.data).object);
2113
+ const objectId = providerId(object.id) ?? "";
2114
+ const candidates = metadataCandidates(object);
2115
+ const identity = {};
2116
+ const identityKeys = {
2117
+ appId: "odlaAppId",
2118
+ chapterId: "chapterId",
2119
+ environment: "odlaEnvironment",
2120
+ dataEnvironment: "odlaDataEnvironment",
2121
+ runtime: "odlaRuntime"
2122
+ };
2123
+ for (const [target, source] of Object.entries(identityKeys)) {
2124
+ const value = metadataValue(candidates, source);
2125
+ if (value) identity[target] = value;
2126
+ }
2127
+ let objectKind = "ignored";
2128
+ if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
2129
+ else if (eventType.startsWith("customer.")) objectKind = "customer";
2130
+ else if (eventType.startsWith("invoice.")) objectKind = "invoice";
2131
+ else if (eventType.startsWith("refund.")) objectKind = "refund";
2132
+ else if (eventType === "charge.refunded") objectKind = "refund";
2133
+ const customerId = refId(object.customer, "cus_");
2134
+ const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
2135
+ const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
2136
+ const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
2137
+ const paymentIntentId = refId(object.payment_intent, "pi_");
2138
+ const price = firstPrice(object);
2139
+ const amountCents = integer(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
2140
+ const currency = text(object.currency)?.toLowerCase();
2141
+ const status = text(object.status);
2142
+ return {
2143
+ eventId,
2144
+ digest,
2145
+ eventType,
2146
+ eventCreated: integer(event.created) ?? 0,
2147
+ livemode: event.livemode === true,
2148
+ objectKind,
2149
+ objectId,
2150
+ identity,
2151
+ ...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
2152
+ ...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
2153
+ ...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
2154
+ ...customerId ? { customerId } : {},
2155
+ ...subscriptionId ? { subscriptionId } : {},
2156
+ ...invoiceId ? { invoiceId } : {},
2157
+ ...chargeId ? { chargeId } : {},
2158
+ ...paymentIntentId ? { paymentIntentId } : {},
2159
+ ...price,
2160
+ ...status ? { status } : {},
2161
+ ...currency ? { currency } : {},
2162
+ ...amountCents !== void 0 ? { amountCents } : {},
2163
+ ...integer(object.amount_paid) !== void 0 ? { amountPaidCents: integer(object.amount_paid) } : {},
2164
+ ...integer(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer(object.amount_remaining) } : {},
2165
+ ...integer(object.current_period_start) !== void 0 ? { currentPeriodStart: integer(object.current_period_start) } : {},
2166
+ ...integer(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer(object.current_period_end) } : {},
2167
+ ...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
2168
+ };
2169
+ }
2170
+ var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2171
+ function directSource(fact, sources) {
2172
+ const mode = fact.livemode ? "live" : "test";
2173
+ const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
2174
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
2175
+ if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2176
+ if (supplied.length > 0 && candidates.length === 0) return "wrong";
2177
+ return null;
2178
+ }
2179
+ var objectRefs = (fact) => [
2180
+ fact.customerId && `customer:${fact.customerId}`,
2181
+ fact.subscriptionId && `subscription:${fact.subscriptionId}`,
2182
+ fact.invoiceId && `invoice:${fact.invoiceId}`,
2183
+ fact.chargeId && `charge:${fact.chargeId}`,
2184
+ fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
2185
+ ].filter((value) => Boolean(value));
2186
+ function ownRef(fact) {
2187
+ if (!fact.objectId) return null;
2188
+ if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
2189
+ if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
2190
+ if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
2191
+ if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
2192
+ return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
2193
+ }
2194
+ var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
2195
+ function projectNetworkCommercialEvents(input, sources) {
2196
+ const facts2 = [...input].sort(order);
2197
+ const exceptions2 = [];
2198
+ const unique = [];
2199
+ const digests = /* @__PURE__ */ new Map();
2200
+ for (const fact of facts2) {
2201
+ const prior = digests.get(fact.eventId);
2202
+ if (prior === void 0) {
2203
+ digests.set(fact.eventId, fact.digest);
2204
+ unique.push(fact);
2205
+ } else if (prior !== fact.digest) exceptions2.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2206
+ }
2207
+ const assignments = /* @__PURE__ */ new Map();
2208
+ const refs = /* @__PURE__ */ new Map();
2209
+ for (const fact of unique) {
2210
+ const direct = directSource(fact, sources);
2211
+ if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
2212
+ }
2213
+ let changed = true;
2214
+ while (changed) {
2215
+ changed = false;
2216
+ for (const fact of unique) {
2217
+ const assigned = assignments.get(fact.eventId);
2218
+ if (assigned) {
2219
+ const key = networkCommercialSourceKey(assigned);
2220
+ for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
2221
+ const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
2222
+ const before = keys2.size;
2223
+ keys2.add(key);
2224
+ refs.set(ref, keys2);
2225
+ if (keys2.size !== before) changed = true;
2226
+ }
2227
+ continue;
2228
+ }
2229
+ if (directSource(fact, sources) === "wrong") continue;
2230
+ const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
2231
+ if (keys.size !== 1) continue;
2232
+ const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
2233
+ if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
2234
+ assignments.set(fact.eventId, candidate);
2235
+ changed = true;
2236
+ }
2237
+ }
2238
+ }
2239
+ const latest = /* @__PURE__ */ new Map();
2240
+ for (const fact of unique) {
2241
+ if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
2242
+ exceptions2.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2243
+ continue;
2244
+ }
2245
+ const source = assignments.get(fact.eventId);
2246
+ if (!source) {
2247
+ const direct = directSource(fact, sources);
2248
+ const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
2249
+ exceptions2.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
2250
+ continue;
2251
+ }
2252
+ const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
2253
+ const key = `${fact.objectKind}:${fact.objectId}`;
2254
+ const prior = latest.get(key);
2255
+ if (!prior || order(prior, fact) <= 0) latest.set(key, row);
2256
+ }
2257
+ const rows3 = [...latest.values()];
2258
+ const customers = rows3.filter((row) => row.objectKind === "customer");
2259
+ const subscriptions = rows3.filter((row) => row.objectKind === "subscription");
2260
+ const invoices = rows3.filter((row) => row.objectKind === "invoice");
2261
+ const refunds = rows3.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
2262
+ for (const charge of rows3.filter((row) => row.eventType === "charge.refunded")) {
2263
+ if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
2264
+ exceptions2.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
2265
+ }
2266
+ }
2267
+ const totalsMap = /* @__PURE__ */ new Map();
2268
+ const add = (row, field, amount) => {
2269
+ if (!row.currency) return;
2270
+ const key = `${row.sourceKey}:${row.currency}`;
2271
+ const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2272
+ total[field] += amount;
2273
+ total.netCents = total.paidCents - total.refundedCents;
2274
+ totalsMap.set(key, total);
2275
+ };
2276
+ for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
2277
+ for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
2278
+ return { customers, subscriptions, invoices, refunds, exceptions: exceptions2, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
2279
+ }
2280
+
2281
+ // src/network-commercial-reconcile.ts
2282
+ var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2283
+ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
2284
+ const objects = [];
2285
+ let startingAfter;
2286
+ for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
2287
+ const page = await client.request("GET", path, {
2288
+ ...params,
2289
+ limit: 100,
2290
+ ...startingAfter ? { starting_after: startingAfter } : {}
2291
+ });
2292
+ if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
2293
+ throw new Error("Stripe returned an invalid commercial list response");
2294
+ }
2295
+ const rows3 = page.data.map(asRaw).filter((row) => row !== null);
2296
+ if (rows3.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
2297
+ objects.push(...rows3);
2298
+ if (!page.has_more) return { objects, complete: true };
2299
+ const cursor = rows3.at(-1)?.id;
2300
+ if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
2301
+ startingAfter = cursor;
2302
+ }
2303
+ return { objects, complete: false };
2304
+ }
2305
+ function facts(snapshots) {
2306
+ const groups2 = [
2307
+ ["customer.updated", snapshots.customers.objects],
2308
+ ["customer.subscription.updated", snapshots.subscriptions.objects],
2309
+ ["invoice.updated", snapshots.invoices.objects],
2310
+ ["refund.updated", snapshots.refunds.objects]
2311
+ ];
2312
+ return groups2.flatMap(([eventType, objects]) => objects.map((object) => {
2313
+ const id6 = typeof object.id === "string" ? object.id : "invalid";
2314
+ return normalizeNetworkCommercialStripeEvent({
2315
+ id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id6}`,
2316
+ type: eventType,
2317
+ created: typeof object.created === "number" ? object.created : 0,
2318
+ livemode: object.livemode === true,
2319
+ data: { object }
2320
+ }, `stripe-snapshot:${eventType}:${id6}`);
2321
+ }));
2322
+ }
2323
+ var selected = (row) => ({
2324
+ status: row.status ?? null,
2325
+ currency: row.currency ?? null,
2326
+ amountCents: row.amountCents ?? null,
2327
+ amountPaidCents: row.amountPaidCents ?? null,
2328
+ amountRemainingCents: row.amountRemainingCents ?? null,
2329
+ customerId: row.customerId ?? null,
2330
+ subscriptionId: row.subscriptionId ?? null,
2331
+ invoiceId: row.invoiceId ?? null,
2332
+ priceId: row.priceId ?? null,
2333
+ quantity: row.quantity ?? null,
2334
+ applicationId: row.applicationId ?? null,
2335
+ personId: row.personId ?? null,
2336
+ membershipId: row.membershipId ?? null,
2337
+ currentPeriodStart: row.currentPeriodStart ?? null,
2338
+ currentPeriodEnd: row.currentPeriodEnd ?? null,
2339
+ cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
2340
+ });
2341
+ var index = (projection2) => new Map(
2342
+ [...projection2.customers, ...projection2.subscriptions, ...projection2.invoices, ...projection2.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
2343
+ );
2344
+ function reconcileStripeSnapshot(shadow, snapshots, sources) {
2345
+ const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
2346
+ const shadowRows = index(shadow);
2347
+ const stripeRows = index(stripe);
2348
+ const exceptions2 = [];
2349
+ const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
2350
+ if (!complete) exceptions2.push({ kind: "stripe_snapshot_truncated", detail: {} });
2351
+ for (const [key, row] of shadowRows) {
2352
+ const other = stripeRows.get(key);
2353
+ if (!other) {
2354
+ exceptions2.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
2355
+ } else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
2356
+ exceptions2.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
2357
+ }
2358
+ }
2359
+ for (const [key, row] of stripeRows) {
2360
+ if (!shadowRows.has(key)) exceptions2.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
2361
+ }
2362
+ if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
2363
+ exceptions2.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
2364
+ }
2365
+ return {
2366
+ ok: exceptions2.length === 0,
2367
+ complete,
2368
+ ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
2369
+ exceptions: exceptions2,
2370
+ stripeTotals: stripe.totals
2371
+ };
2372
+ }
2373
+
2374
+ // src/network-commercial-parity.ts
2375
+ var DIGEST = /^[0-9a-f]{64}$/;
2376
+ var MAX_RECORDS = 500;
2377
+ var text2 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
2378
+ function parityMessage(partition, kind, value) {
2379
+ return [
2380
+ "chapter-commercial-parity",
2381
+ "v1",
2382
+ partition.appId,
2383
+ partition.chapterId,
2384
+ partition.environment,
2385
+ partition.dataEnvironment,
2386
+ partition.runtime,
2387
+ kind,
2388
+ value
2389
+ ].join("");
2390
+ }
2391
+ async function parityKey(secret) {
2392
+ const encoder4 = new TextEncoder();
2393
+ return crypto.subtle.importKey(
2394
+ "raw",
2395
+ encoder4.encode(secret),
2396
+ { name: "HMAC", hash: "SHA-256" },
2397
+ false,
2398
+ ["sign"]
2399
+ );
2400
+ }
2401
+ async function digestWithKey(key, partition, kind, value) {
2402
+ const encoder4 = new TextEncoder();
2403
+ const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
2404
+ return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2405
+ }
2406
+ async function commercialParityDigest(secret, partition, kind, value) {
2407
+ return digestWithKey(await parityKey(secret), partition, kind, value);
2408
+ }
2409
+ async function buildChapterCommercialParitySnapshot(input) {
2410
+ const requestedMax = input.maxRecords ?? MAX_RECORDS;
2411
+ const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
2412
+ const eligible = input.rows.filter((row) => text2(row.id) && (text2(row.stripeCustomerId) || text2(row.stripeSubscriptionId)));
2413
+ const selected2 = eligible.slice(0, maxRecords);
2414
+ const key = await parityKey(input.secret);
2415
+ const records = await Promise.all(selected2.map(async (row) => {
2416
+ const applicationId = text2(row.id);
2417
+ const customerId = text2(row.stripeCustomerId);
2418
+ const subscriptionId = text2(row.stripeSubscriptionId);
2419
+ return {
2420
+ applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
2421
+ customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
2422
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
2423
+ status: text2(row.status, 64),
2424
+ canceled: row.canceled === true
2425
+ };
2426
+ }));
2427
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2428
+ const truncated = input.scanTruncated || eligible.length > maxRecords;
2429
+ return {
2430
+ version: 1,
2431
+ site: { id: input.siteId },
2432
+ partition: input.partition,
2433
+ generatedAt: input.now ?? Date.now(),
2434
+ complete: !truncated,
2435
+ truncated,
2436
+ counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
2437
+ records
2438
+ };
2439
+ }
2440
+ function recordKey(record4) {
2441
+ return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
2442
+ }
2443
+ function validSnapshot(snapshot, source) {
2444
+ const partition = snapshot?.partition;
2445
+ const records = snapshot?.records;
2446
+ 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;
2447
+ const seen = /* @__PURE__ */ new Set();
2448
+ for (const record4 of records) {
2449
+ 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;
2450
+ const key = recordKey(record4);
2451
+ if (seen.has(key)) return false;
2452
+ seen.add(key);
2453
+ }
2454
+ 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;
2455
+ }
2456
+ async function reconcileCommercialParitySnapshot(input) {
2457
+ if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
2458
+ const sourceKey = networkCommercialSourceKey(input.source);
2459
+ const key = await parityKey(input.secret);
2460
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text2(row.applicationId) && text2(row.objectId)).map(async (row) => {
2461
+ const customerId = text2(row.customerId);
2462
+ const subscriptionId = text2(row.subscriptionId) ?? text2(row.objectId);
2463
+ return {
2464
+ applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
2465
+ customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
2466
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
2467
+ status: null,
2468
+ canceled: false,
2469
+ stripeStatus: text2(row.status, 64),
2470
+ stripeCanceled: row.status === "canceled"
2471
+ };
2472
+ }));
2473
+ const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
2474
+ const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
2475
+ const exceptions2 = [];
2476
+ const records = [];
2477
+ for (const [key2, chapter] of chapterByKey) {
2478
+ const stripe = shadowByKey.get(key2);
2479
+ if (!stripe) {
2480
+ exceptions2.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
2481
+ continue;
2482
+ }
2483
+ const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
2484
+ records.push(compared);
2485
+ if (chapter.canceled !== stripe.stripeCanceled) {
2486
+ exceptions2.push({
2487
+ kind: "cancellation_mismatch",
2488
+ applicationDigest: chapter.applicationDigest,
2489
+ detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
2490
+ });
2491
+ }
2492
+ }
2493
+ for (const [key2, stripe] of shadowByKey) {
2494
+ if (!chapterByKey.has(key2)) {
2495
+ exceptions2.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
2496
+ }
2497
+ }
2498
+ if (!input.snapshot.complete || input.snapshot.truncated) {
2499
+ exceptions2.push({
2500
+ kind: "chapter_truncated",
2501
+ detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
2502
+ });
2503
+ }
2504
+ if (chapterByKey.size !== shadowByKey.size) {
2505
+ exceptions2.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
2506
+ }
2507
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2508
+ return {
2509
+ ok: input.snapshot.complete && exceptions2.length === 0,
2510
+ complete: input.snapshot.complete,
2511
+ counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
2512
+ records,
2513
+ exceptions: exceptions2
2514
+ };
2515
+ }
2516
+
1980
2517
  // src/leader.ts
1981
2518
  import { composeCrmConfigs } from "@odla-ai/crm";
1982
2519
  function leaderCrmConfig(options = {}) {
@@ -2145,14 +2682,14 @@ import { createCrmIntegration } from "@odla-ai/crm";
2145
2682
 
2146
2683
  // src/tiers.ts
2147
2684
  function buildTierSeeds(groupId, configured = [], now = 0) {
2148
- return configured.map((tier, index) => ({
2685
+ return configured.map((tier, index2) => ({
2149
2686
  id: tier.id,
2150
2687
  groupId,
2151
2688
  name: tier.name,
2152
2689
  priceCents: tier.priceCents,
2153
2690
  ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
2154
2691
  ...tier.blurb ? { blurb: tier.blurb } : {},
2155
- sortOrder: tier.sortOrder ?? index,
2692
+ sortOrder: tier.sortOrder ?? index2,
2156
2693
  active: tier.active !== false,
2157
2694
  createdAt: now
2158
2695
  }));
@@ -2225,13 +2762,26 @@ function createChapterIntegration(chapter, options = {}) {
2225
2762
  const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
2226
2763
  const runbooks = isLeader ? {
2227
2764
  role: "leader",
2228
- slugs: ["chapter-leader", "chapter-network", "crm"],
2765
+ slugs: ["chapter-leader", "chapter-network", "crm", ...chapter.networkCommercialReadModel ? ["stripe-network-shadow"] : []],
2229
2766
  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\`.`
2230
2767
  } : {
2231
2768
  role: "follower",
2232
2769
  slugs: ["chapter-follower", "chapter-network"],
2233
2770
  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\`.`
2234
2771
  };
2772
+ const secrets = [];
2773
+ if (chapter.account !== "none") secrets.push({
2774
+ // Released Chapter workers request this effective name. odla-db maps it to
2775
+ // the canonical reserved `$clerk_secret` value written by the CLI.
2776
+ name: "clerk_secret_key",
2777
+ description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
2778
+ required: true
2779
+ });
2780
+ if (chapter.networkCommercialReadModel) secrets.push({
2781
+ name: "network_commercial_stripe_webhook_secret",
2782
+ description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
2783
+ required: true
2784
+ });
2235
2785
  return {
2236
2786
  id: "chapter",
2237
2787
  title: `Chapter: ${chapter.name}`,
@@ -2242,14 +2792,7 @@ function createChapterIntegration(chapter, options = {}) {
2242
2792
  },
2243
2793
  rules: { ...crmDesc.rules, ...chapter.rules },
2244
2794
  seeds,
2245
- secrets: chapter.account === "none" ? [] : [{
2246
- // Released Chapter workers request this effective name. odla-db maps
2247
- // it to the canonical reserved `$clerk_secret` value written by
2248
- // `odla-ai secrets set-clerk-key`.
2249
- name: "clerk_secret_key",
2250
- description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
2251
- required: true
2252
- }],
2795
+ secrets,
2253
2796
  probes: [
2254
2797
  ...crmDesc.probes ?? [],
2255
2798
  // The snapshot route is deliberately private. A credential-free smoke
@@ -2260,6 +2803,7 @@ function createChapterIntegration(chapter, options = {}) {
2260
2803
  path: "/api/network/shared-notes",
2261
2804
  expectedStatus: 401
2262
2805
  }] : [],
2806
+ ...chapter.network.readers.some((reader) => reader.commercialParity) ? [{ path: "/api/network/commercial-parity", expectedStatus: 401 }] : [],
2263
2807
  // Formation metadata is public only when the host explicitly enables
2264
2808
  // the bounded intake contract.
2265
2809
  ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
@@ -2272,7 +2816,11 @@ function createChapterIntegration(chapter, options = {}) {
2272
2816
  { path: "/api/admin/builder-library", expectedStatus: 401 },
2273
2817
  { path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
2274
2818
  ] : [],
2275
- ...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : []
2819
+ ...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : [],
2820
+ ...chapter.networkCommercialReadModel ? [
2821
+ { path: "/api/network-commercial/stripe-webhook", expectedStatus: 405 },
2822
+ { path: "/api/admin/network-commercial-read-model", expectedStatus: 401 }
2823
+ ] : []
2276
2824
  ],
2277
2825
  runbooks
2278
2826
  };
@@ -2280,7 +2828,7 @@ function createChapterIntegration(chapter, options = {}) {
2280
2828
 
2281
2829
  // src/member-content-validation-helpers.ts
2282
2830
  var ID = /^[a-z][a-z0-9-]{0,79}$/;
2283
- var SLUG7 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2831
+ var SLUG8 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2284
2832
  var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2285
2833
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2286
2834
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
@@ -2312,21 +2860,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2312
2860
  return valueText;
2313
2861
  }
2314
2862
  function offerValue(value, path, errors, tiers2, audience) {
2315
- const raw = record(value);
2316
- if (!raw) {
2863
+ const raw2 = record(value);
2864
+ if (!raw2) {
2317
2865
  errors.push(`${path} must be an object`);
2318
2866
  return void 0;
2319
2867
  }
2320
- unknownKeys(raw, OFFER_KEYS, path, errors);
2321
- const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2868
+ unknownKeys(raw2, OFFER_KEYS, path, errors);
2869
+ const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2322
2870
  if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2323
2871
  if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2324
2872
  if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2325
2873
  errors.push(`${path}.tierId must grant access to this page`);
2326
2874
  }
2327
- const label = textValue(raw.label, `${path}.label`, errors);
2328
- const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
2329
- const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2875
+ const label = textValue(raw2.label, `${path}.label`, errors);
2876
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
2877
+ const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
2330
2878
  return {
2331
2879
  tierId,
2332
2880
  ...label ? { label } : {},
@@ -2378,37 +2926,37 @@ function validateMemberContentDocument(input, options = {}) {
2378
2926
  const ids = /* @__PURE__ */ new Set();
2379
2927
  const slugs = /* @__PURE__ */ new Set();
2380
2928
  const pages = [];
2381
- for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2382
- const path = `document.pages[${index}]`;
2383
- const raw = record(value);
2384
- if (!raw) {
2929
+ for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2930
+ const path = `document.pages[${index2}]`;
2931
+ const raw2 = record(value);
2932
+ if (!raw2) {
2385
2933
  errors.push(`${path} must be an object`);
2386
2934
  continue;
2387
2935
  }
2388
- unknownKeys(raw, PAGE_KEYS, path, errors);
2389
- const id5 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2390
- const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2391
- const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2392
- if (id5 && !ID.test(id5)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2393
- if (slug && !SLUG7.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2394
- if (ids.has(id5) || knownParentIds.has(id5)) errors.push(`${path}.id duplicates page "${id5}"`);
2936
+ unknownKeys(raw2, PAGE_KEYS, path, errors);
2937
+ const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
2938
+ const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
2939
+ const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2940
+ if (id6 && !ID.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2941
+ if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2942
+ if (ids.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
2395
2943
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2396
- ids.add(id5);
2944
+ ids.add(id6);
2397
2945
  slugs.add(slug);
2398
- const navRaw = record(raw.navigation);
2399
- let navigation = { section: "General", order: index };
2946
+ const navRaw = record(raw2.navigation);
2947
+ let navigation = { section: "General", order: index2 };
2400
2948
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
2401
2949
  else {
2402
2950
  unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2403
2951
  const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2404
- const order = navRaw.order;
2405
- if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
2952
+ const order2 = navRaw.order;
2953
+ if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
2406
2954
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2407
2955
  }
2408
2956
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2409
- navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
2957
+ navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
2410
2958
  }
2411
- const audienceRaw = record(raw.audience);
2959
+ const audienceRaw = record(raw2.audience);
2412
2960
  let audience = { kind: "all-members" };
2413
2961
  if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2414
2962
  else {
@@ -2430,12 +2978,12 @@ function validateMemberContentDocument(input, options = {}) {
2430
2978
  audience = { kind: "tiers", tierIds: normalized };
2431
2979
  } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2432
2980
  }
2433
- const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2434
- const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
2435
- const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
2981
+ const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
2982
+ const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
2983
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
2436
2984
  let upsell;
2437
- if (raw.upsell !== void 0) {
2438
- const upsellRaw = record(raw.upsell);
2985
+ if (raw2.upsell !== void 0) {
2986
+ const upsellRaw = record(raw2.upsell);
2439
2987
  if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2440
2988
  else {
2441
2989
  unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
@@ -2449,13 +2997,13 @@ function validateMemberContentDocument(input, options = {}) {
2449
2997
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2450
2998
  }
2451
2999
  }
2452
- pages.push({ id: id5, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
3000
+ pages.push({ id: id6, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2453
3001
  }
2454
3002
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2455
- for (const [index, page] of pages.entries()) {
3003
+ for (const [index2, page] of pages.entries()) {
2456
3004
  const parentId = page.navigation.parentId;
2457
- if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2458
- if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
3005
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
3006
+ if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
2459
3007
  }
2460
3008
  for (const page of pages) {
2461
3009
  const seen = /* @__PURE__ */ new Set([page.id]);
@@ -2476,21 +3024,21 @@ function validateMemberContentDocument(input, options = {}) {
2476
3024
  const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2477
3025
  const seen = /* @__PURE__ */ new Set();
2478
3026
  upsellOverrides = [];
2479
- for (const [index, value] of values.entries()) {
2480
- const path = `document.upsellOverrides[${index}]`;
2481
- const raw = record(value);
2482
- if (!raw) {
3027
+ for (const [index2, value] of values.entries()) {
3028
+ const path = `document.upsellOverrides[${index2}]`;
3029
+ const raw2 = record(value);
3030
+ if (!raw2) {
2483
3031
  errors.push(`${path} must be an object`);
2484
3032
  continue;
2485
3033
  }
2486
- unknownKeys(raw, OVERRIDE_KEYS, path, errors);
2487
- const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
3034
+ unknownKeys(raw2, OVERRIDE_KEYS, path, errors);
3035
+ const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2488
3036
  if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2489
3037
  seen.add(pageId);
2490
3038
  const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2491
3039
  if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2492
3040
  else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2493
- const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
3041
+ const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2494
3042
  if (offer) upsellOverrides.push({ pageId, offer });
2495
3043
  }
2496
3044
  }
@@ -2615,7 +3163,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
2615
3163
  function unknownKeys2(value, allowed2, path, errors) {
2616
3164
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2617
3165
  }
2618
- function text(value, path, errors, max = MAX_TEXT2, required = true) {
3166
+ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
2619
3167
  if (value === void 0 && !required) return void 0;
2620
3168
  if (typeof value !== "string" || required && !value.trim()) {
2621
3169
  errors.push(`${path} must be a non-empty string`);
@@ -2625,23 +3173,23 @@ function text(value, path, errors, max = MAX_TEXT2, required = true) {
2625
3173
  if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
2626
3174
  return result;
2627
3175
  }
2628
- function integer(value, path, errors, minimum = 1) {
3176
+ function integer2(value, path, errors, minimum = 1) {
2629
3177
  if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
2630
3178
  return Number(value);
2631
3179
  }
2632
3180
  function scopeValue(value, path, errors) {
2633
- const raw = record2(value);
2634
- if (!raw) {
3181
+ const raw2 = record2(value);
3182
+ if (!raw2) {
2635
3183
  errors.push(`${path} must be an object`);
2636
3184
  return null;
2637
3185
  }
2638
- unknownKeys2(raw, SCOPE_KEYS, path, errors);
2639
- if (raw.kind === "global" || raw.kind === "built_not_found") {
2640
- if (raw.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
2641
- return { kind: raw.kind };
3186
+ unknownKeys2(raw2, SCOPE_KEYS, path, errors);
3187
+ if (raw2.kind === "global" || raw2.kind === "built_not_found") {
3188
+ if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
3189
+ return { kind: raw2.kind };
2642
3190
  }
2643
- if (raw.kind === "chapter") {
2644
- const chapterId = text(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3191
+ if (raw2.kind === "chapter") {
3192
+ const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
2645
3193
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
2646
3194
  return { kind: "chapter", chapterId };
2647
3195
  }
@@ -2649,28 +3197,28 @@ function scopeValue(value, path, errors) {
2649
3197
  return null;
2650
3198
  }
2651
3199
  function resourceValue(value, path, errors) {
2652
- const raw = record2(value);
2653
- if (!raw) {
3200
+ const raw2 = record2(value);
3201
+ if (!raw2) {
2654
3202
  errors.push(`${path} must be an object`);
2655
3203
  return null;
2656
3204
  }
2657
- unknownKeys2(raw, RESOURCE_KEYS, path, errors);
2658
- const id5 = text(raw.id, `${path}.id`, errors, 80) ?? "";
2659
- if (id5 && !ID2.test(id5)) errors.push(`${path}.id must be lowercase kebab-case`);
2660
- const kind = text(raw.kind, `${path}.kind`, errors) ?? "";
3205
+ unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
3206
+ const id6 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
3207
+ if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
3208
+ const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
2661
3209
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
2662
- const title = text(raw.title, `${path}.title`, errors, 240) ?? "";
2663
- const summary = text(raw.summary, `${path}.summary`, errors, 1e3, false);
2664
- const status = text(raw.status, `${path}.status`, errors) ?? "";
3210
+ const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
3211
+ const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
3212
+ const status = text3(raw2.status, `${path}.status`, errors) ?? "";
2665
3213
  if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
2666
- const revision = integer(raw.revision, `${path}.revision`, errors);
2667
- const scope = scopeValue(raw.scope, `${path}.scope`, errors) ?? { kind: "global" };
2668
- const tags = Array.isArray(raw.tags) ? raw.tags.map((tag, index) => text(tag, `${path}.tags[${index}]`, errors, 80) ?? "") : [];
2669
- if (!Array.isArray(raw.tags)) errors.push(`${path}.tags must be an array`);
3214
+ const revision = integer2(raw2.revision, `${path}.revision`, errors);
3215
+ const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
3216
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
3217
+ if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
2670
3218
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
2671
3219
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
2672
- const bodyMarkdown = text(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
2673
- const url = text(raw.url, `${path}.url`, errors, 2048, false);
3220
+ const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
3221
+ const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
2674
3222
  if (url) {
2675
3223
  try {
2676
3224
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -2678,13 +3226,13 @@ function resourceValue(value, path, errors) {
2678
3226
  errors.push(`${path}.url must be a valid https URL`);
2679
3227
  }
2680
3228
  }
2681
- const asset = raw.brandAsset === void 0 ? null : record2(raw.brandAsset);
2682
- if (raw.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
3229
+ const asset = raw2.brandAsset === void 0 ? null : record2(raw2.brandAsset);
3230
+ if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
2683
3231
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
2684
3232
  const brandAsset = asset ? {
2685
- bookId: text(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
2686
- assetId: text(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
2687
- ...asset.filename === void 0 ? {} : { filename: text(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
3233
+ bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
3234
+ assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
3235
+ ...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
2688
3236
  } : void 0;
2689
3237
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
2690
3238
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -2692,59 +3240,59 @@ function resourceValue(value, path, errors) {
2692
3240
  if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
2693
3241
  if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
2694
3242
  if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
2695
- const provenanceRaw = record2(raw.provenance);
3243
+ const provenanceRaw = record2(raw2.provenance);
2696
3244
  if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
2697
3245
  if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
2698
3246
  const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
2699
3247
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
2700
3248
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
2701
3249
  const promotedFrom = promotedRaw ? {
2702
- chapterId: text(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
2703
- resourceId: text(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
2704
- resourceRevision: integer(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
2705
- documentRevision: integer(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
2706
- promotedBy: text(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
2707
- promotedAt: integer(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
3250
+ chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
3251
+ resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
3252
+ resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
3253
+ documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
3254
+ promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3255
+ promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
2708
3256
  } : void 0;
2709
3257
  const provenance = {
2710
- createdBy: text(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
2711
- createdAt: integer(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
2712
- updatedBy: text(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
2713
- updatedAt: integer(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
3258
+ createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3259
+ createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
3260
+ updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3261
+ updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
2714
3262
  ...promotedFrom ? { promotedFrom } : {}
2715
3263
  };
2716
- return { id: id5, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
3264
+ return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
2717
3265
  }
2718
3266
  function validateBuilderLibraryDocument(value, options = {}) {
2719
3267
  const errors = [];
2720
- const raw = record2(value);
2721
- if (!raw) return { ok: false, errors: ["document must be an object"] };
2722
- unknownKeys2(raw, ROOT_KEYS2, "document", errors);
2723
- if (raw.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2724
- const revision = integer(raw.revision, "document.revision", errors);
2725
- const generatedAt = integer(raw.generatedAt, "document.generatedAt", errors, 0);
2726
- const sourceRaw = record2(raw.source);
3268
+ const raw2 = record2(value);
3269
+ if (!raw2) return { ok: false, errors: ["document must be an object"] };
3270
+ unknownKeys2(raw2, ROOT_KEYS2, "document", errors);
3271
+ if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
3272
+ const revision = integer2(raw2.revision, "document.revision", errors);
3273
+ const generatedAt = integer2(raw2.generatedAt, "document.generatedAt", errors, 0);
3274
+ const sourceRaw = record2(raw2.source);
2727
3275
  if (!sourceRaw) errors.push("document.source must be an object");
2728
3276
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
2729
3277
  const sourceType = sourceRaw?.type;
2730
3278
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
2731
- const sourceAppId = text(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
3279
+ const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
2732
3280
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
2733
3281
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2734
3282
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2735
- const targetRaw = raw.target === void 0 ? null : record2(raw.target);
2736
- if (raw.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
3283
+ const targetRaw = raw2.target === void 0 ? null : record2(raw2.target);
3284
+ if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
2737
3285
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
2738
3286
  const target = targetRaw ? {
2739
- appId: text(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
2740
- environment: text(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3287
+ appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3288
+ environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
2741
3289
  } : void 0;
2742
3290
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
2743
- if (!Array.isArray(raw.resources)) errors.push("document.resources must be an array");
2744
- const inputs = Array.isArray(raw.resources) ? raw.resources : [];
3291
+ if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
3292
+ const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
2745
3293
  if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
2746
- const resources = inputs.map((item, index) => resourceValue(item, `document.resources[${index}]`, errors)).filter(Boolean);
2747
- const ids = resources.map(({ id: id5 }) => id5);
3294
+ const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
3295
+ const ids = resources.map(({ id: id6 }) => id6);
2748
3296
  if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
2749
3297
  for (const resource of resources) {
2750
3298
  if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
@@ -2861,10 +3409,10 @@ function planDelivery(input) {
2861
3409
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
2862
3410
  const to = redirect ? input.group.debugEmail : input.to;
2863
3411
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
2864
- const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
3412
+ const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
2865
3413
 
2866
3414
  ` + render(tpl.text, vars) : render(tpl.text, vars);
2867
- return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
3415
+ return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
2868
3416
  }
2869
3417
 
2870
3418
  // src/notify.ts
@@ -2899,9 +3447,9 @@ async function sendTemplated(deps, input) {
2899
3447
  error = e instanceof Error ? e.message : String(e);
2900
3448
  }
2901
3449
  }
2902
- const id5 = deps.newId();
3450
+ const id6 = deps.newId();
2903
3451
  const row = {
2904
- id: id5,
3452
+ id: id6,
2905
3453
  groupId: input.group.id,
2906
3454
  to: decision.to,
2907
3455
  template: input.template,
@@ -2915,7 +3463,7 @@ async function sendTemplated(deps, input) {
2915
3463
  ...messageId ? { messageId } : {},
2916
3464
  ...error ? { error } : {}
2917
3465
  };
2918
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id5, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
3466
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
2919
3467
  return error ? { sent: false, reason: error } : { sent: true };
2920
3468
  }
2921
3469
  function emailGroupFrom(row) {
@@ -3101,29 +3649,29 @@ function validateAdmissionGrantDocument(input) {
3101
3649
  const grants = [];
3102
3650
  const seenIds = /* @__PURE__ */ new Set();
3103
3651
  const seenEmails = /* @__PURE__ */ new Set();
3104
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
3105
- const grant = raw && typeof raw === "object" ? raw : {};
3106
- const id5 = typeof grant.id === "string" ? grant.id : "";
3652
+ for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
3653
+ const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
3654
+ const id6 = typeof grant.id === "string" ? grant.id : "";
3107
3655
  const email = typeof grant.email === "string" ? grant.email.trim() : "";
3108
3656
  const normalized = normalizeAdmissionEmail(email);
3109
3657
  const decision = grant.decision;
3110
3658
  const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
3111
3659
  const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
3112
- if (!ID3.test(id5)) errors.push(`document.grants[${index}].id must be an identifier`);
3113
- if (seenIds.has(id5)) errors.push(`document.grants repeats id "${id5}"`);
3114
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
3660
+ if (!ID3.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
3661
+ if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
3662
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
3115
3663
  if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
3116
3664
  if (decision !== "waive_interview" && decision !== "require_interview") {
3117
- errors.push(`document.grants[${index}].decision is invalid`);
3665
+ errors.push(`document.grants[${index2}].decision is invalid`);
3118
3666
  }
3119
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
3667
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
3120
3668
  if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
3121
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
3669
+ errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
3122
3670
  }
3123
- seenIds.add(id5);
3671
+ seenIds.add(id6);
3124
3672
  seenEmails.add(normalized);
3125
3673
  grants.push({
3126
- id: id5,
3674
+ id: id6,
3127
3675
  email,
3128
3676
  decision,
3129
3677
  ...note ? { note } : {},
@@ -3194,8 +3742,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3194
3742
  const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
3195
3743
  const ops = [];
3196
3744
  for (const row of current) {
3197
- const id5 = String(row.id ?? "");
3198
- if (id5 && !incomingIds.has(id5)) ops.push({ t: "update", ns: "admissionGrants", id: id5, attrs: {
3745
+ const id6 = String(row.id ?? "");
3746
+ if (id6 && !incomingIds.has(id6)) ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
3199
3747
  active: false,
3200
3748
  revokedAt: receivedAt,
3201
3749
  revokedBy: document.source.actor,
@@ -3204,10 +3752,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3204
3752
  } });
3205
3753
  }
3206
3754
  for (const grant of document.grants) {
3207
- const id5 = `admission:parent:${document.source.appId}:${grant.id}`;
3208
- const existing = current.find((row) => row.id === id5);
3209
- ops.push({ t: "update", ns: "admissionGrants", id: id5, attrs: {
3210
- id: id5,
3755
+ const id6 = `admission:parent:${document.source.appId}:${grant.id}`;
3756
+ const existing = current.find((row) => row.id === id6);
3757
+ ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
3758
+ id: id6,
3211
3759
  sourceGrantId: grant.id,
3212
3760
  sourceScope: "parent",
3213
3761
  sourceId: document.source.appId,
@@ -3228,7 +3776,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
3228
3776
  ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
3229
3777
  ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
3230
3778
  ];
3231
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id5, attrs: retract });
3779
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id6, attrs: retract });
3232
3780
  }
3233
3781
  if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
3234
3782
  const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
@@ -3270,7 +3818,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
3270
3818
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3271
3819
  var CURRENCY = /^[a-z]{3}$/;
3272
3820
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
3273
- var text2 = (value, path, errors, max) => {
3821
+ var text4 = (value, path, errors, max) => {
3274
3822
  if (typeof value !== "string" || !value.trim()) {
3275
3823
  errors.push(`${path} is required`);
3276
3824
  return "";
@@ -3289,32 +3837,32 @@ function validateGiftOfferCatalog(input, tierIds) {
3289
3837
  if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
3290
3838
  const ids = /* @__PURE__ */ new Set();
3291
3839
  const offers = [];
3292
- for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
3293
- const path = `document.giftOffers.offers[${index}]`;
3294
- const item = record3(raw);
3840
+ for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
3841
+ const path = `document.giftOffers.offers[${index2}]`;
3842
+ const item = record3(raw2);
3295
3843
  if (!item) {
3296
3844
  errors.push(`${path} must be an object`);
3297
3845
  continue;
3298
3846
  }
3299
3847
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
3300
3848
  for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
3301
- const id5 = text2(item.id, `${path}.id`, errors, 80);
3302
- if (id5 && !ID4.test(id5)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3303
- if (ids.has(id5)) errors.push(`${path}.id duplicates "${id5}"`);
3304
- ids.add(id5);
3849
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
3850
+ if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3851
+ if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
3852
+ ids.add(id6);
3305
3853
  const targetRaw = record3(item.target);
3306
3854
  let target = { type: "entitlement", entitlementKey: "invalid" };
3307
3855
  if (targetRaw?.type === "tier") {
3308
- const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3856
+ const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3309
3857
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
3310
3858
  target = { type: "tier", tierId };
3311
3859
  } else if (targetRaw?.type === "entitlement") {
3312
- const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3860
+ const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3313
3861
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
3314
3862
  target = { type: "entitlement", entitlementKey };
3315
3863
  } else errors.push(`${path}.target.type must be tier or entitlement`);
3316
3864
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
3317
- const stripePriceId = text2(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3865
+ const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3318
3866
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
3319
3867
  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`);
3320
3868
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -3327,7 +3875,7 @@ function validateGiftOfferCatalog(input, tierIds) {
3327
3875
  if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
3328
3876
  if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
3329
3877
  offers.push({
3330
- id: id5,
3878
+ id: id6,
3331
3879
  target,
3332
3880
  billingMode: item.billingMode,
3333
3881
  stripePriceId,
@@ -3336,9 +3884,9 @@ function validateGiftOfferCatalog(input, tierIds) {
3336
3884
  redemptionLifetimeHours: lifetime,
3337
3885
  ...activeFrom !== void 0 ? { activeFrom } : {},
3338
3886
  ...activeUntil !== void 0 ? { activeUntil } : {},
3339
- title: text2(item.title, `${path}.title`, errors, 160),
3340
- description: text2(item.description, `${path}.description`, errors, 2e3),
3341
- ctaLabel: text2(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3887
+ title: text4(item.title, `${path}.title`, errors, 160),
3888
+ description: text4(item.description, `${path}.description`, errors, 2e3),
3889
+ ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3342
3890
  });
3343
3891
  }
3344
3892
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -3383,8 +3931,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
3383
3931
  const typeKey = type.toLowerCase();
3384
3932
  const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
3385
3933
  const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
3386
- const raw = `${prefix}:${hubRecordId}`;
3387
- if (readable && raw.length <= 64) return raw;
3934
+ const raw2 = `${prefix}:${hubRecordId}`;
3935
+ if (readable && raw2.length <= 64) return raw2;
3388
3936
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
3389
3937
  }
3390
3938
  function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
@@ -3654,14 +4202,14 @@ function periodSeries(points, now, count, periodMs) {
3654
4202
  for (const point of points) {
3655
4203
  if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
3656
4204
  if (point.t >= start && point.t <= now) {
3657
- const index = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
3658
- current[index] = (current[index] ?? 0) + point.v;
4205
+ const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
4206
+ current[index2] = (current[index2] ?? 0) + point.v;
3659
4207
  } else if (point.t >= previousStart && point.t < start) {
3660
- const index = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
3661
- previous[index] = (previous[index] ?? 0) + point.v;
4208
+ const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
4209
+ previous[index2] = (previous[index2] ?? 0) + point.v;
3662
4210
  }
3663
4211
  }
3664
- const labels = current.map((_, index) => new Date(start + index * periodMs).toISOString().slice(5, 10));
4212
+ const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
3665
4213
  return { labels, current, previous };
3666
4214
  }
3667
4215
  function dashboardMetricData(points, now) {
@@ -3745,9 +4293,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
3745
4293
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
3746
4294
  if (!found.ok) return false;
3747
4295
  const users = await found.json().catch(() => null);
3748
- const id5 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
3749
- if (!id5) return false;
3750
- return updateClerkUserMetadata(secretKey, id5, input, fetchImpl);
4296
+ const id6 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
4297
+ if (!id6) return false;
4298
+ return updateClerkUserMetadata(secretKey, id6, input, fetchImpl);
3751
4299
  }
3752
4300
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
3753
4301
  const { path, body } = clerkUserRequest(input);
@@ -3810,8 +4358,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
3810
4358
  const user = Array.isArray(data) ? data[0] : void 0;
3811
4359
  return user ? toRecord(user) : null;
3812
4360
  }
3813
- async function clerkGetUser(secretKey, id5, fetchImpl = fetch) {
3814
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id5)}`, secretKey, fetchImpl).catch(() => null);
4361
+ async function clerkGetUser(secretKey, id6, fetchImpl = fetch) {
4362
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id6)}`, secretKey, fetchImpl).catch(() => null);
3815
4363
  return data ? toRecord(data) : null;
3816
4364
  }
3817
4365
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -3827,8 +4375,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
3827
4375
  }
3828
4376
  return out;
3829
4377
  }
3830
- async function clerkSetRole(secretKey, id5, role, fetchImpl = fetch) {
3831
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id5)}/metadata`, {
4378
+ async function clerkSetRole(secretKey, id6, role, fetchImpl = fetch) {
4379
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id6)}/metadata`, {
3832
4380
  method: "PATCH",
3833
4381
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
3834
4382
  body: JSON.stringify({ public_metadata: { role } })
@@ -4054,8 +4602,8 @@ function chapterBrandFromTokens(compiled, base = {}) {
4054
4602
  function isReconcilable(meeting, now) {
4055
4603
  return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
4056
4604
  }
4057
- function reconcileMeetings(meetings2, events, now) {
4058
- const byEvent = new Map(events.map((e) => [e.eventId, e]));
4605
+ function reconcileMeetings(meetings2, events2, now) {
4606
+ const byEvent = new Map(events2.map((e) => [e.eventId, e]));
4059
4607
  const decisions = [];
4060
4608
  for (const m of meetings2) {
4061
4609
  if (!isReconcilable(m, now) || !m.googleEventId) continue;
@@ -4265,10 +4813,10 @@ function boundedString(value, path, errors, max, required = false) {
4265
4813
  errors.push(`${path} must be a string`);
4266
4814
  return void 0;
4267
4815
  }
4268
- const text4 = value.trim();
4269
- if (required && !text4) errors.push(`${path} is required`);
4270
- if (text4.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4271
- return text4;
4816
+ const text6 = value.trim();
4817
+ if (required && !text6) errors.push(`${path} is required`);
4818
+ if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4819
+ return text6;
4272
4820
  }
4273
4821
  function validateMembershipPageDocument(input, options = {}) {
4274
4822
  const errors = [];
@@ -4286,9 +4834,9 @@ function validateMembershipPageDocument(input, options = {}) {
4286
4834
  const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
4287
4835
  const seen = /* @__PURE__ */ new Set();
4288
4836
  const tiers2 = [];
4289
- for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
4290
- const path = `document.tiers[${index}]`;
4291
- const item = objectRecord(raw);
4837
+ for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
4838
+ const path = `document.tiers[${index2}]`;
4839
+ const item = objectRecord(raw2);
4292
4840
  if (!item) {
4293
4841
  errors.push(`${path} must be an object`);
4294
4842
  continue;
@@ -4317,8 +4865,8 @@ function validateMembershipPageDocument(input, options = {}) {
4317
4865
  else {
4318
4866
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
4319
4867
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
4320
- const text4 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4321
- return text4 ? [text4] : [];
4868
+ const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4869
+ return text6 ? [text6] : [];
4322
4870
  });
4323
4871
  }
4324
4872
  }
@@ -4394,7 +4942,7 @@ function membershipPageDocument(input, options = {}) {
4394
4942
  }
4395
4943
 
4396
4944
  // src/signup-control-validate.ts
4397
- var SLUG8 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4945
+ var SLUG9 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4398
4946
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
4399
4947
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
4400
4948
  var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
@@ -4417,7 +4965,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
4417
4965
  function unknownKeys3(value, allowed2, path, errors) {
4418
4966
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
4419
4967
  }
4420
- function text3(value, path, errors, max = 65536) {
4968
+ function text5(value, path, errors, max = 65536) {
4421
4969
  if (typeof value !== "string" || !value.trim()) {
4422
4970
  errors.push(`${path} is required`);
4423
4971
  return "";
@@ -4432,19 +4980,19 @@ function tiersOf(value, errors) {
4432
4980
  }
4433
4981
  if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
4434
4982
  const seen = /* @__PURE__ */ new Set();
4435
- return value.slice(0, 20).flatMap((raw, index) => {
4436
- const path = `document.tiers[${index}]`;
4437
- const item = recordOf(raw);
4983
+ return value.slice(0, 20).flatMap((raw2, index2) => {
4984
+ const path = `document.tiers[${index2}]`;
4985
+ const item = recordOf(raw2);
4438
4986
  if (!item) {
4439
4987
  errors.push(`${path} must be an object`);
4440
4988
  return [];
4441
4989
  }
4442
4990
  unknownKeys3(item, TIER_KEYS2, path, errors);
4443
- const id5 = text3(item.id, `${path}.id`, errors, 80);
4444
- if (id5 && !TIER.test(id5)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4445
- if (seen.has(id5)) errors.push(`${path}.id duplicates "${id5}"`);
4446
- seen.add(id5);
4447
- const name = text3(item.name, `${path}.name`, errors, 160);
4991
+ const id6 = text5(item.id, `${path}.id`, errors, 80);
4992
+ if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4993
+ if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
4994
+ seen.add(id6);
4995
+ const name = text5(item.name, `${path}.name`, errors, 160);
4448
4996
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
4449
4997
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
4450
4998
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -4456,9 +5004,9 @@ function tiersOf(value, errors) {
4456
5004
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
4457
5005
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
4458
5006
  }
4459
- const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
5007
+ const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
4460
5008
  return [{
4461
- id: id5,
5009
+ id: id6,
4462
5010
  name,
4463
5011
  available: item.available === true,
4464
5012
  sortOrder: Number(item.sortOrder) || 0,
@@ -4512,7 +5060,7 @@ function policyOf(value, errors) {
4512
5060
  const result = {};
4513
5061
  for (const key of allowed2) {
4514
5062
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
4515
- result[key] = text3(input[key], `document.policy.${key}`, errors);
5063
+ result[key] = text5(input[key], `document.policy.${key}`, errors);
4516
5064
  }
4517
5065
  return result;
4518
5066
  }
@@ -4550,18 +5098,18 @@ function validateSignupControlDocument(input) {
4550
5098
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
4551
5099
  const source = recordOf(root.source);
4552
5100
  const target = recordOf(root.target);
4553
- const appId = text3(source?.appId, "document.source.appId", errors, 63);
4554
- const chapterId = text3(target?.chapterId, "document.target.chapterId", errors, 63);
4555
- const environment = text3(target?.environment, "document.target.environment", errors, 63);
4556
- if (appId && !SLUG8.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
4557
- if (chapterId && !SLUG8.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
5101
+ const appId = text5(source?.appId, "document.source.appId", errors, 63);
5102
+ const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
5103
+ const environment = text5(target?.environment, "document.target.environment", errors, 63);
5104
+ if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
5105
+ if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
4558
5106
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
4559
5107
  if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
4560
5108
  const tiers2 = tiersOf(root.tiers, errors);
4561
5109
  const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
4562
5110
  const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
4563
5111
  if (!page.ok) errors.push(...page.errors);
4564
- else if (page.value.tiers.some((tier, index) => tier.tierId !== available[index]?.id)) {
5112
+ else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
4565
5113
  errors.push("document.membershipPage.tiers must follow the available tier display order");
4566
5114
  }
4567
5115
  const application = applicationOf(root.application, errors);
@@ -4596,7 +5144,7 @@ function validateSignupControlDocument(input) {
4596
5144
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
4597
5145
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
4598
5146
  var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
4599
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
5147
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
4600
5148
  var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
4601
5149
  var MAX_TOKENS = 256;
4602
5150
  var MAX_VALUE_LENGTH = 1e3;
@@ -4608,14 +5156,14 @@ function validatedTokens(input) {
4608
5156
  }
4609
5157
  const tokens = {};
4610
5158
  let serializedLength = 0;
4611
- for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
5159
+ for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
4612
5160
  if (!TOKEN_NAME.test(name)) {
4613
5161
  throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
4614
5162
  }
4615
- if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
5163
+ if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
4616
5164
  throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
4617
5165
  }
4618
- const value = String(raw).trim();
5166
+ const value = String(raw2).trim();
4619
5167
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
4620
5168
  throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
4621
5169
  }
@@ -4640,7 +5188,7 @@ function stateFor(revision, options) {
4640
5188
  metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
4641
5189
  };
4642
5190
  }
4643
- if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
5191
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
4644
5192
  throw new TypeError("signup-control theme revision metadata is invalid");
4645
5193
  }
4646
5194
  return {
@@ -4722,16 +5270,16 @@ function markdownSection(rawHeading) {
4722
5270
  }
4723
5271
  function markdownBenefits(lines, path, errors) {
4724
5272
  const benefits = [];
4725
- for (const [index, line] of lines.entries()) {
5273
+ for (const [index2, line] of lines.entries()) {
4726
5274
  if (!line.trim()) continue;
4727
5275
  const match = line.match(MARKDOWN_BENEFIT);
4728
5276
  if (!match) {
4729
- errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
5277
+ errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
4730
5278
  continue;
4731
5279
  }
4732
- const text4 = (match[1] ?? "").trim();
4733
- if (text4.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
4734
- if (text4) benefits.push(text4);
5280
+ const text6 = (match[1] ?? "").trim();
5281
+ if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
5282
+ if (text6) benefits.push(text6);
4735
5283
  }
4736
5284
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
4737
5285
  return benefits.slice(0, MAX_BENEFITS2);
@@ -4746,7 +5294,7 @@ function applyMembershipTierMarkdown(presentation, source) {
4746
5294
  const sections = /* @__PURE__ */ new Map();
4747
5295
  const prelude = [];
4748
5296
  let current = null;
4749
- for (const [index, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
5297
+ for (const [index2, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
4750
5298
  const heading = line.match(MARKDOWN_SECTION);
4751
5299
  if (heading) {
4752
5300
  const headingText = heading[1] ?? "";
@@ -4766,7 +5314,7 @@ function applyMembershipTierMarkdown(presentation, source) {
4766
5314
  continue;
4767
5315
  }
4768
5316
  if (MARKDOWN_HEADING.test(line)) {
4769
- errors.push(`membership Markdown line ${index + 1} uses an unsupported heading`);
5317
+ errors.push(`membership Markdown line ${index2 + 1} uses an unsupported heading`);
4770
5318
  continue;
4771
5319
  }
4772
5320
  if (current) sections.get(current)?.lines.push(line);
@@ -4833,7 +5381,7 @@ var exclusions = (chapterId, paidTierIds) => [
4833
5381
  id: chapterId,
4834
5382
  reason: "provider_identity",
4835
5383
  paths: [
4836
- ...paidTierIds.map((id5) => `tiers[${id5}].stripePriceId`),
5384
+ ...paidTierIds.map((id6) => `tiers[${id6}].stripePriceId`),
4837
5385
  "groups.stripePublishableKey",
4838
5386
  "applications.stripeCustomerId",
4839
5387
  "applications.stripeSubscriptionId",
@@ -4875,9 +5423,9 @@ async function exportChapterPromotion(input) {
4875
5423
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
4876
5424
  receiptDigests: [input.digest]
4877
5425
  };
4878
- const artifact = (kind, id5, content) => ({
5426
+ const artifact = (kind, id6, content) => ({
4879
5427
  kind,
4880
- id: id5,
5428
+ id: id6,
4881
5429
  sourceRevision: revision,
4882
5430
  provenance: promotionProvenance,
4883
5431
  content: canonicalPromotionValue(content)
@@ -4916,7 +5464,7 @@ async function exportChapterPromotion(input) {
4916
5464
  }
4917
5465
 
4918
5466
  // src/member-system-authority.ts
4919
- var rows2 = (group) => group.fields.map((id5) => ({ ...group, id: id5 }));
5467
+ var rows2 = (group) => group.fields.map((id6) => ({ ...group, id: id6 }));
4920
5468
  var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
4921
5469
  ...rows2({
4922
5470
  scope: "network",
@@ -5301,10 +5849,10 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
5301
5849
  if (supplied.length === keys.length) return "apply";
5302
5850
  return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
5303
5851
  }
5304
- function planMemberSystemEventReplay(events) {
5852
+ function planMemberSystemEventReplay(events2) {
5305
5853
  const seen = /* @__PURE__ */ new Map();
5306
5854
  const plan = { accepted: [], replayed: [], collisions: [] };
5307
- for (const event of events) {
5855
+ for (const event of events2) {
5308
5856
  const digest = seen.get(event.id);
5309
5857
  if (digest === void 0) {
5310
5858
  seen.set(event.id, event.digest);
@@ -5398,6 +5946,7 @@ export {
5398
5946
  brandTokens,
5399
5947
  bucketSeries,
5400
5948
  buildBuilderLibraryManifest,
5949
+ buildChapterCommercialParitySnapshot,
5401
5950
  buildGroupSeed,
5402
5951
  buildTierSeeds,
5403
5952
  builderLibraryDigest,
@@ -5423,6 +5972,7 @@ export {
5423
5972
  clerkListUsers,
5424
5973
  clerkSetRole,
5425
5974
  clerkUserRequest,
5975
+ commercialParityDigest,
5426
5976
  composeMemberContent,
5427
5977
  createAdmissionGrantEnvelope,
5428
5978
  createBuilderLibraryEnvelope,
@@ -5459,6 +6009,7 @@ export {
5459
6009
  joinConfig,
5460
6010
  joinConfigTiers,
5461
6011
  leaderCrmConfig,
6012
+ listStripeObjects,
5462
6013
  loadLatestAdmissionRevision,
5463
6014
  loadOfferableTiers,
5464
6015
  loadTiers,
@@ -5473,9 +6024,11 @@ export {
5473
6024
  memberSystemRollbackPlan,
5474
6025
  membershipPageDocument,
5475
6026
  membershipTierPresentationFromMarkdown,
6027
+ networkCommercialSourceKey,
5476
6028
  networkSourceTag,
5477
6029
  normalizeAdmissionEmail,
5478
6030
  normalizeChapterEmail,
6031
+ normalizeNetworkCommercialStripeEvent,
5479
6032
  normalizeSharedRecord,
5480
6033
  normalizeWebhookEvent,
5481
6034
  offerableTiers,
@@ -5488,10 +6041,13 @@ export {
5488
6041
  projectBuilderLibraryDelivery,
5489
6042
  projectMemberContent,
5490
6043
  projectMemberSelf,
6044
+ projectNetworkCommercialEvents,
5491
6045
  projectSharedRecord,
5492
6046
  promoteBuilderLibraryResource,
5493
6047
  publicGiftOffers,
6048
+ reconcileCommercialParitySnapshot,
5494
6049
  reconcileMeetings,
6050
+ reconcileStripeSnapshot,
5495
6051
  refundedPatch,
5496
6052
  render,
5497
6053
  renderSignupControlThemeHead,
@@ -5505,6 +6061,7 @@ export {
5505
6061
  resolveJoinContinuation,
5506
6062
  resolveLeaderFormation,
5507
6063
  resolveMemberContent,
6064
+ resolveNetworkCommercialReadModel,
5508
6065
  resolveNetworkPersonRegistry,
5509
6066
  resolvePipeline,
5510
6067
  resolveScheduling,