@odla-ai/chapter 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/{copy-context-B4SCy6vx.d.ts → copy-context-CIxmneHI.d.ts} +24 -0
- package/dist/index.cjs +687 -279
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -3
- package/dist/index.d.ts +134 -3
- package/dist/index.js +687 -279
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +903 -305
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +24 -0
- package/dist/worker/index.d.ts +24 -0
- package/dist/worker/index.js +903 -305
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
|
119
|
-
const row = { id:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
618
|
+
var id5 = () => attr5("string", { unique: true, indexed: true });
|
|
561
619
|
var admins = {
|
|
562
620
|
attrs: {
|
|
563
|
-
id:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
932
|
-
return typeof
|
|
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(
|
|
1412
|
-
const
|
|
1413
|
-
return
|
|
1470
|
+
function humanize(id6) {
|
|
1471
|
+
const text5 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1472
|
+
return text5 ? `${text5[0].toUpperCase()}${text5.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(
|
|
1481
|
+
function applicationType(id6, def) {
|
|
1423
1482
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1424
|
-
return
|
|
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((
|
|
1429
|
-
const def = defs[
|
|
1430
|
-
const conditions = application.conditions[
|
|
1431
|
-
const type = applicationType(
|
|
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:
|
|
1434
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
1492
|
+
id: id6,
|
|
1493
|
+
label: def?.label ?? APPLICATION_LABELS[id6] ?? humanize(id6),
|
|
1435
1494
|
type,
|
|
1436
|
-
required: application.required.includes(
|
|
1437
|
-
order,
|
|
1438
|
-
maxLength: application.maxLen[
|
|
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((
|
|
1449
|
-
const field = def.fields[
|
|
1507
|
+
return [...formation.required, ...formation.optional].map((id6, order2) => {
|
|
1508
|
+
const field = def.fields[id6];
|
|
1450
1509
|
return {
|
|
1451
|
-
id:
|
|
1452
|
-
label: field.label ?? humanize(
|
|
1510
|
+
id: id6,
|
|
1511
|
+
label: field.label ?? humanize(id6),
|
|
1453
1512
|
type: field.type,
|
|
1454
|
-
required: formation.required.includes(
|
|
1455
|
-
order,
|
|
1456
|
-
maxLength: formation.maxLen[
|
|
1513
|
+
required: formation.required.includes(id6),
|
|
1514
|
+
order: order2,
|
|
1515
|
+
maxLength: formation.maxLen[id6] ?? formation.defaultMaxLen,
|
|
1457
1516
|
...field.options ? { options: [...field.options] } : {},
|
|
1458
|
-
...
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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`);
|
|
@@ -1757,22 +1816,50 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
|
|
|
1757
1816
|
if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
|
|
1758
1817
|
const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
|
|
1759
1818
|
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((
|
|
1819
|
+
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id6) => !SLUG2.test(id6))) {
|
|
1761
1820
|
throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
|
|
1762
1821
|
}
|
|
1763
|
-
const unknown = chapterIds.filter((
|
|
1822
|
+
const unknown = chapterIds.filter((id6) => !targetIds.includes(id6));
|
|
1764
1823
|
if (unknown.length) {
|
|
1765
|
-
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((
|
|
1824
|
+
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id6) => `"${id6}"`).join(", ")} are not configured network targets`);
|
|
1766
1825
|
}
|
|
1767
1826
|
return { mode: "shadow", chapterIds };
|
|
1768
1827
|
}
|
|
1769
1828
|
|
|
1829
|
+
// src/config-network-commercial.ts
|
|
1830
|
+
var SLUG3 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
1831
|
+
var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
1832
|
+
function networkCommercialSourceKey(source) {
|
|
1833
|
+
return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
|
|
1834
|
+
}
|
|
1835
|
+
function resolveNetworkCommercialReadModel(mode, input, targetIds) {
|
|
1836
|
+
if (!input) return null;
|
|
1837
|
+
if (mode !== "hub") throw new Error("defineChapter.networkCommercialReadModel: only hub mode may own the network commercial projection");
|
|
1838
|
+
if (input.mode !== "shadow") throw new Error('defineChapter.networkCommercialReadModel.mode: must be "shadow"');
|
|
1839
|
+
if (!Array.isArray(input.sources) || input.sources.length === 0) {
|
|
1840
|
+
throw new Error("defineChapter.networkCommercialReadModel.sources: at least one exact source is required");
|
|
1841
|
+
}
|
|
1842
|
+
const sources = input.sources.map((source) => ({ ...source }));
|
|
1843
|
+
for (const source of sources) {
|
|
1844
|
+
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") {
|
|
1845
|
+
throw new Error("defineChapter.networkCommercialReadModel.sources: every source requires valid app, chapter, environment, data-environment, runtime, and Stripe mode values");
|
|
1846
|
+
}
|
|
1847
|
+
if (!targetIds.includes(source.chapterId)) {
|
|
1848
|
+
throw new Error(`defineChapter.networkCommercialReadModel.sources: chapter "${source.chapterId}" is not a configured network target`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (new Set(sources.map(networkCommercialSourceKey)).size !== sources.length) {
|
|
1852
|
+
throw new Error("defineChapter.networkCommercialReadModel.sources: exact source partitions must be distinct");
|
|
1853
|
+
}
|
|
1854
|
+
return { mode: "shadow", sources };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1770
1857
|
// src/config-signup-control.ts
|
|
1771
|
-
var
|
|
1858
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1772
1859
|
var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1773
1860
|
function resolveSignupControl(input) {
|
|
1774
1861
|
if (!input) return null;
|
|
1775
|
-
if (!
|
|
1862
|
+
if (!SLUG4.test(input.sourceId)) {
|
|
1776
1863
|
throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
|
|
1777
1864
|
}
|
|
1778
1865
|
if (input.stripeMode !== "test" && input.stripeMode !== "live") {
|
|
@@ -1786,12 +1873,12 @@ function resolveSignupControl(input) {
|
|
|
1786
1873
|
}
|
|
1787
1874
|
|
|
1788
1875
|
// src/config-member-content.ts
|
|
1789
|
-
var
|
|
1876
|
+
var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1790
1877
|
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1791
1878
|
function resolveMemberContent(input) {
|
|
1792
1879
|
if (!input) return null;
|
|
1793
1880
|
const parent = input.parent;
|
|
1794
|
-
if (parent && !
|
|
1881
|
+
if (parent && !SLUG5.test(parent.sourceId)) {
|
|
1795
1882
|
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1796
1883
|
}
|
|
1797
1884
|
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
@@ -1806,22 +1893,22 @@ function resolveMemberContent(input) {
|
|
|
1806
1893
|
}
|
|
1807
1894
|
|
|
1808
1895
|
// src/config-builder-library.ts
|
|
1809
|
-
var
|
|
1896
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1810
1897
|
function resolveBuilderLibrary(mode, input, network = { targets: [], readers: [] }) {
|
|
1811
1898
|
if (!input) return null;
|
|
1812
1899
|
if (input.sources !== void 0 && !Array.isArray(input.sources)) {
|
|
1813
1900
|
throw new Error("defineChapter.builderLibrary.sources: must be an array");
|
|
1814
1901
|
}
|
|
1815
1902
|
const seen = /* @__PURE__ */ new Set();
|
|
1816
|
-
const sources = (input.sources ?? []).map((source,
|
|
1817
|
-
if (!source || typeof source !== "object" || !
|
|
1818
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
1903
|
+
const sources = (input.sources ?? []).map((source, index2) => {
|
|
1904
|
+
if (!source || typeof source !== "object" || !SLUG6.test(source.sourceId)) {
|
|
1905
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}].sourceId: must be a lowercase app slug`);
|
|
1819
1906
|
}
|
|
1820
1907
|
if (source.sourceId === "built-not-found" && mode === "hub") {
|
|
1821
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
1908
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}]: a hub accepts Chapter sources, not itself`);
|
|
1822
1909
|
}
|
|
1823
1910
|
if (mode === "chapter" && source.sourceId !== "built-not-found") {
|
|
1824
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
1911
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}]: Chapters may accept only the built-not-found source`);
|
|
1825
1912
|
}
|
|
1826
1913
|
if (seen.has(source.sourceId)) {
|
|
1827
1914
|
throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
|
|
@@ -1829,14 +1916,14 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
|
|
|
1829
1916
|
seen.add(source.sourceId);
|
|
1830
1917
|
if ("secretName" in source) {
|
|
1831
1918
|
throw new Error(
|
|
1832
|
-
`defineChapter.builderLibrary.sources[${
|
|
1919
|
+
`defineChapter.builderLibrary.sources[${index2}].secretName: remove this field; Builder Library reuses the Chapter network edge`
|
|
1833
1920
|
);
|
|
1834
1921
|
}
|
|
1835
1922
|
const networkPeer = mode === "hub" ? network.targets.some((target) => target.id === source.sourceId) : network.readers.some((reader) => reader.id === source.sourceId);
|
|
1836
1923
|
if (!networkPeer) {
|
|
1837
1924
|
const side = mode === "hub" ? "targets" : "readers";
|
|
1838
1925
|
throw new Error(
|
|
1839
|
-
`defineChapter.builderLibrary.sources[${
|
|
1926
|
+
`defineChapter.builderLibrary.sources[${index2}]: "${source.sourceId}" must already be configured in network.${side}`
|
|
1840
1927
|
);
|
|
1841
1928
|
}
|
|
1842
1929
|
return { sourceId: source.sourceId };
|
|
@@ -1845,15 +1932,15 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
|
|
|
1845
1932
|
}
|
|
1846
1933
|
|
|
1847
1934
|
// src/config.ts
|
|
1848
|
-
var
|
|
1935
|
+
var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1849
1936
|
function isResolvedCrm(x) {
|
|
1850
1937
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1851
1938
|
}
|
|
1852
1939
|
function defineChapter(config) {
|
|
1853
1940
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1854
|
-
const { id:
|
|
1855
|
-
if (typeof
|
|
1856
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
1941
|
+
const { id: id6, name } = config;
|
|
1942
|
+
if (typeof id6 !== "string" || !SLUG7.test(id6)) {
|
|
1943
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id6)}`);
|
|
1857
1944
|
}
|
|
1858
1945
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1859
1946
|
const mode = config.mode ?? "chapter";
|
|
@@ -1881,6 +1968,11 @@ function defineChapter(config) {
|
|
|
1881
1968
|
config.networkPersonRegistry,
|
|
1882
1969
|
network.targets.map((target) => target.id)
|
|
1883
1970
|
);
|
|
1971
|
+
const networkCommercialReadModel = resolveNetworkCommercialReadModel(
|
|
1972
|
+
mode,
|
|
1973
|
+
config.networkCommercialReadModel,
|
|
1974
|
+
network.targets.map((target) => target.id)
|
|
1975
|
+
);
|
|
1884
1976
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1885
1977
|
const memberContent = resolveMemberContent(config.memberContent);
|
|
1886
1978
|
const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary, network);
|
|
@@ -1900,7 +1992,8 @@ function defineChapter(config) {
|
|
|
1900
1992
|
Boolean(memberContent),
|
|
1901
1993
|
Boolean(builderLibrary),
|
|
1902
1994
|
network.targets.some((target) => target.admissionGrants),
|
|
1903
|
-
Boolean(networkPersonRegistry)
|
|
1995
|
+
Boolean(networkPersonRegistry),
|
|
1996
|
+
Boolean(networkCommercialReadModel)
|
|
1904
1997
|
);
|
|
1905
1998
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1906
1999
|
const account = config.account ?? "none";
|
|
@@ -1950,7 +2043,7 @@ function defineChapter(config) {
|
|
|
1950
2043
|
};
|
|
1951
2044
|
const chapter = {
|
|
1952
2045
|
config,
|
|
1953
|
-
id:
|
|
2046
|
+
id: id6,
|
|
1954
2047
|
name,
|
|
1955
2048
|
mode,
|
|
1956
2049
|
brand,
|
|
@@ -1960,6 +2053,7 @@ function defineChapter(config) {
|
|
|
1960
2053
|
memberContent,
|
|
1961
2054
|
builderLibrary,
|
|
1962
2055
|
networkPersonRegistry,
|
|
2056
|
+
networkCommercialReadModel,
|
|
1963
2057
|
formation,
|
|
1964
2058
|
crm,
|
|
1965
2059
|
auth,
|
|
@@ -1977,6 +2071,304 @@ function defineChapter(config) {
|
|
|
1977
2071
|
return chapter;
|
|
1978
2072
|
}
|
|
1979
2073
|
|
|
2074
|
+
// src/network-commercial.ts
|
|
2075
|
+
var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2076
|
+
var text = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
|
|
2077
|
+
var integer = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
2078
|
+
var bool = (value) => typeof value === "boolean" ? value : void 0;
|
|
2079
|
+
var providerId = (value, prefix) => {
|
|
2080
|
+
const valueText = text(value);
|
|
2081
|
+
return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
|
|
2082
|
+
};
|
|
2083
|
+
function metadataCandidates(object) {
|
|
2084
|
+
const parent = raw(object.parent);
|
|
2085
|
+
const subscriptionDetails = raw(object.subscription_details);
|
|
2086
|
+
const parentSubscription = raw(parent.subscription_details);
|
|
2087
|
+
return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
|
|
2088
|
+
}
|
|
2089
|
+
function metadataValue(candidates, key) {
|
|
2090
|
+
return candidates.map((candidate) => text(candidate[key])).find(Boolean);
|
|
2091
|
+
}
|
|
2092
|
+
function refId(value, prefix) {
|
|
2093
|
+
if (typeof value === "string") return providerId(value, prefix);
|
|
2094
|
+
return providerId(raw(value).id, prefix);
|
|
2095
|
+
}
|
|
2096
|
+
function subscriptionRef(object) {
|
|
2097
|
+
return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
|
|
2098
|
+
}
|
|
2099
|
+
function firstPrice(object) {
|
|
2100
|
+
const data = raw(object.items).data;
|
|
2101
|
+
const item = Array.isArray(data) ? raw(data[0]) : {};
|
|
2102
|
+
const priceId = refId(item.price, "price_");
|
|
2103
|
+
const quantity = integer(item.quantity);
|
|
2104
|
+
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
2105
|
+
}
|
|
2106
|
+
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
2107
|
+
const event = raw(input);
|
|
2108
|
+
const eventId = providerId(event.id, "evt_") ?? "";
|
|
2109
|
+
const eventType = text(event.type) ?? "";
|
|
2110
|
+
const object = raw(raw(event.data).object);
|
|
2111
|
+
const objectId = providerId(object.id) ?? "";
|
|
2112
|
+
const candidates = metadataCandidates(object);
|
|
2113
|
+
const identity = {};
|
|
2114
|
+
const identityKeys = {
|
|
2115
|
+
appId: "odlaAppId",
|
|
2116
|
+
chapterId: "chapterId",
|
|
2117
|
+
environment: "odlaEnvironment",
|
|
2118
|
+
dataEnvironment: "odlaDataEnvironment",
|
|
2119
|
+
runtime: "odlaRuntime"
|
|
2120
|
+
};
|
|
2121
|
+
for (const [target, source] of Object.entries(identityKeys)) {
|
|
2122
|
+
const value = metadataValue(candidates, source);
|
|
2123
|
+
if (value) identity[target] = value;
|
|
2124
|
+
}
|
|
2125
|
+
let objectKind = "ignored";
|
|
2126
|
+
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
2127
|
+
else if (eventType.startsWith("customer.")) objectKind = "customer";
|
|
2128
|
+
else if (eventType.startsWith("invoice.")) objectKind = "invoice";
|
|
2129
|
+
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
2130
|
+
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
2131
|
+
const customerId = refId(object.customer, "cus_");
|
|
2132
|
+
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
|
|
2133
|
+
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
|
|
2134
|
+
const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
|
|
2135
|
+
const paymentIntentId = refId(object.payment_intent, "pi_");
|
|
2136
|
+
const price = firstPrice(object);
|
|
2137
|
+
const amountCents = integer(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
|
|
2138
|
+
const currency = text(object.currency)?.toLowerCase();
|
|
2139
|
+
const status = text(object.status);
|
|
2140
|
+
return {
|
|
2141
|
+
eventId,
|
|
2142
|
+
digest,
|
|
2143
|
+
eventType,
|
|
2144
|
+
eventCreated: integer(event.created) ?? 0,
|
|
2145
|
+
livemode: event.livemode === true,
|
|
2146
|
+
objectKind,
|
|
2147
|
+
objectId,
|
|
2148
|
+
identity,
|
|
2149
|
+
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
2150
|
+
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
2151
|
+
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
2152
|
+
...customerId ? { customerId } : {},
|
|
2153
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
2154
|
+
...invoiceId ? { invoiceId } : {},
|
|
2155
|
+
...chargeId ? { chargeId } : {},
|
|
2156
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
2157
|
+
...price,
|
|
2158
|
+
...status ? { status } : {},
|
|
2159
|
+
...currency ? { currency } : {},
|
|
2160
|
+
...amountCents !== void 0 ? { amountCents } : {},
|
|
2161
|
+
...integer(object.amount_paid) !== void 0 ? { amountPaidCents: integer(object.amount_paid) } : {},
|
|
2162
|
+
...integer(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer(object.amount_remaining) } : {},
|
|
2163
|
+
...integer(object.current_period_start) !== void 0 ? { currentPeriodStart: integer(object.current_period_start) } : {},
|
|
2164
|
+
...integer(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer(object.current_period_end) } : {},
|
|
2165
|
+
...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
2169
|
+
function directSource(fact, sources) {
|
|
2170
|
+
const mode = fact.livemode ? "live" : "test";
|
|
2171
|
+
const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
|
|
2172
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
|
|
2173
|
+
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
2174
|
+
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
var objectRefs = (fact) => [
|
|
2178
|
+
fact.customerId && `customer:${fact.customerId}`,
|
|
2179
|
+
fact.subscriptionId && `subscription:${fact.subscriptionId}`,
|
|
2180
|
+
fact.invoiceId && `invoice:${fact.invoiceId}`,
|
|
2181
|
+
fact.chargeId && `charge:${fact.chargeId}`,
|
|
2182
|
+
fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
|
|
2183
|
+
].filter((value) => Boolean(value));
|
|
2184
|
+
function ownRef(fact) {
|
|
2185
|
+
if (!fact.objectId) return null;
|
|
2186
|
+
if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
|
|
2187
|
+
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
2188
|
+
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
2189
|
+
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
2190
|
+
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
2191
|
+
}
|
|
2192
|
+
var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
|
|
2193
|
+
function projectNetworkCommercialEvents(input, sources) {
|
|
2194
|
+
const facts2 = [...input].sort(order);
|
|
2195
|
+
const exceptions2 = [];
|
|
2196
|
+
const unique = [];
|
|
2197
|
+
const digests = /* @__PURE__ */ new Map();
|
|
2198
|
+
for (const fact of facts2) {
|
|
2199
|
+
const prior = digests.get(fact.eventId);
|
|
2200
|
+
if (prior === void 0) {
|
|
2201
|
+
digests.set(fact.eventId, fact.digest);
|
|
2202
|
+
unique.push(fact);
|
|
2203
|
+
} else if (prior !== fact.digest) exceptions2.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
2204
|
+
}
|
|
2205
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
2206
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2207
|
+
for (const fact of unique) {
|
|
2208
|
+
const direct = directSource(fact, sources);
|
|
2209
|
+
if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
|
|
2210
|
+
}
|
|
2211
|
+
let changed = true;
|
|
2212
|
+
while (changed) {
|
|
2213
|
+
changed = false;
|
|
2214
|
+
for (const fact of unique) {
|
|
2215
|
+
const assigned = assignments.get(fact.eventId);
|
|
2216
|
+
if (assigned) {
|
|
2217
|
+
const key = networkCommercialSourceKey(assigned);
|
|
2218
|
+
for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
|
|
2219
|
+
const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
|
|
2220
|
+
const before = keys2.size;
|
|
2221
|
+
keys2.add(key);
|
|
2222
|
+
refs.set(ref, keys2);
|
|
2223
|
+
if (keys2.size !== before) changed = true;
|
|
2224
|
+
}
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
if (directSource(fact, sources) === "wrong") continue;
|
|
2228
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
2229
|
+
if (keys.size !== 1) continue;
|
|
2230
|
+
const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
|
|
2231
|
+
if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
|
|
2232
|
+
assignments.set(fact.eventId, candidate);
|
|
2233
|
+
changed = true;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2238
|
+
for (const fact of unique) {
|
|
2239
|
+
if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
|
|
2240
|
+
exceptions2.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
const source = assignments.get(fact.eventId);
|
|
2244
|
+
if (!source) {
|
|
2245
|
+
const direct = directSource(fact, sources);
|
|
2246
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
2247
|
+
exceptions2.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
|
|
2251
|
+
const key = `${fact.objectKind}:${fact.objectId}`;
|
|
2252
|
+
const prior = latest.get(key);
|
|
2253
|
+
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
2254
|
+
}
|
|
2255
|
+
const rows3 = [...latest.values()];
|
|
2256
|
+
const customers = rows3.filter((row) => row.objectKind === "customer");
|
|
2257
|
+
const subscriptions = rows3.filter((row) => row.objectKind === "subscription");
|
|
2258
|
+
const invoices = rows3.filter((row) => row.objectKind === "invoice");
|
|
2259
|
+
const refunds = rows3.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
2260
|
+
for (const charge of rows3.filter((row) => row.eventType === "charge.refunded")) {
|
|
2261
|
+
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
2262
|
+
exceptions2.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
const totalsMap = /* @__PURE__ */ new Map();
|
|
2266
|
+
const add = (row, field, amount) => {
|
|
2267
|
+
if (!row.currency) return;
|
|
2268
|
+
const key = `${row.sourceKey}:${row.currency}`;
|
|
2269
|
+
const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
2270
|
+
total[field] += amount;
|
|
2271
|
+
total.netCents = total.paidCents - total.refundedCents;
|
|
2272
|
+
totalsMap.set(key, total);
|
|
2273
|
+
};
|
|
2274
|
+
for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
|
|
2275
|
+
for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
|
|
2276
|
+
return { customers, subscriptions, invoices, refunds, exceptions: exceptions2, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/network-commercial-reconcile.ts
|
|
2280
|
+
var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2281
|
+
async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
2282
|
+
const objects = [];
|
|
2283
|
+
let startingAfter;
|
|
2284
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
|
|
2285
|
+
const page = await client.request("GET", path, {
|
|
2286
|
+
...params,
|
|
2287
|
+
limit: 100,
|
|
2288
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
2289
|
+
});
|
|
2290
|
+
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
2291
|
+
throw new Error("Stripe returned an invalid commercial list response");
|
|
2292
|
+
}
|
|
2293
|
+
const rows3 = page.data.map(asRaw).filter((row) => row !== null);
|
|
2294
|
+
if (rows3.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
2295
|
+
objects.push(...rows3);
|
|
2296
|
+
if (!page.has_more) return { objects, complete: true };
|
|
2297
|
+
const cursor = rows3.at(-1)?.id;
|
|
2298
|
+
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
2299
|
+
startingAfter = cursor;
|
|
2300
|
+
}
|
|
2301
|
+
return { objects, complete: false };
|
|
2302
|
+
}
|
|
2303
|
+
function facts(snapshots) {
|
|
2304
|
+
const groups2 = [
|
|
2305
|
+
["customer.updated", snapshots.customers.objects],
|
|
2306
|
+
["customer.subscription.updated", snapshots.subscriptions.objects],
|
|
2307
|
+
["invoice.updated", snapshots.invoices.objects],
|
|
2308
|
+
["refund.updated", snapshots.refunds.objects]
|
|
2309
|
+
];
|
|
2310
|
+
return groups2.flatMap(([eventType, objects]) => objects.map((object) => {
|
|
2311
|
+
const id6 = typeof object.id === "string" ? object.id : "invalid";
|
|
2312
|
+
return normalizeNetworkCommercialStripeEvent({
|
|
2313
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id6}`,
|
|
2314
|
+
type: eventType,
|
|
2315
|
+
created: typeof object.created === "number" ? object.created : 0,
|
|
2316
|
+
livemode: object.livemode === true,
|
|
2317
|
+
data: { object }
|
|
2318
|
+
}, `stripe-snapshot:${eventType}:${id6}`);
|
|
2319
|
+
}));
|
|
2320
|
+
}
|
|
2321
|
+
var selected = (row) => ({
|
|
2322
|
+
status: row.status ?? null,
|
|
2323
|
+
currency: row.currency ?? null,
|
|
2324
|
+
amountCents: row.amountCents ?? null,
|
|
2325
|
+
amountPaidCents: row.amountPaidCents ?? null,
|
|
2326
|
+
amountRemainingCents: row.amountRemainingCents ?? null,
|
|
2327
|
+
customerId: row.customerId ?? null,
|
|
2328
|
+
subscriptionId: row.subscriptionId ?? null,
|
|
2329
|
+
invoiceId: row.invoiceId ?? null,
|
|
2330
|
+
priceId: row.priceId ?? null,
|
|
2331
|
+
quantity: row.quantity ?? null,
|
|
2332
|
+
applicationId: row.applicationId ?? null,
|
|
2333
|
+
personId: row.personId ?? null,
|
|
2334
|
+
membershipId: row.membershipId ?? null,
|
|
2335
|
+
currentPeriodStart: row.currentPeriodStart ?? null,
|
|
2336
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
2337
|
+
cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
|
|
2338
|
+
});
|
|
2339
|
+
var index = (projection2) => new Map(
|
|
2340
|
+
[...projection2.customers, ...projection2.subscriptions, ...projection2.invoices, ...projection2.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
|
|
2341
|
+
);
|
|
2342
|
+
function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
2343
|
+
const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
|
|
2344
|
+
const shadowRows = index(shadow);
|
|
2345
|
+
const stripeRows = index(stripe);
|
|
2346
|
+
const exceptions2 = [];
|
|
2347
|
+
const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
|
|
2348
|
+
if (!complete) exceptions2.push({ kind: "stripe_snapshot_truncated", detail: {} });
|
|
2349
|
+
for (const [key, row] of shadowRows) {
|
|
2350
|
+
const other = stripeRows.get(key);
|
|
2351
|
+
if (!other) {
|
|
2352
|
+
exceptions2.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
2353
|
+
} else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
|
|
2354
|
+
exceptions2.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
for (const [key, row] of stripeRows) {
|
|
2358
|
+
if (!shadowRows.has(key)) exceptions2.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
2359
|
+
}
|
|
2360
|
+
if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
|
|
2361
|
+
exceptions2.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
|
|
2362
|
+
}
|
|
2363
|
+
return {
|
|
2364
|
+
ok: exceptions2.length === 0,
|
|
2365
|
+
complete,
|
|
2366
|
+
ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
|
|
2367
|
+
exceptions: exceptions2,
|
|
2368
|
+
stripeTotals: stripe.totals
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
|
|
1980
2372
|
// src/leader.ts
|
|
1981
2373
|
import { composeCrmConfigs } from "@odla-ai/crm";
|
|
1982
2374
|
function leaderCrmConfig(options = {}) {
|
|
@@ -2145,14 +2537,14 @@ import { createCrmIntegration } from "@odla-ai/crm";
|
|
|
2145
2537
|
|
|
2146
2538
|
// src/tiers.ts
|
|
2147
2539
|
function buildTierSeeds(groupId, configured = [], now = 0) {
|
|
2148
|
-
return configured.map((tier,
|
|
2540
|
+
return configured.map((tier, index2) => ({
|
|
2149
2541
|
id: tier.id,
|
|
2150
2542
|
groupId,
|
|
2151
2543
|
name: tier.name,
|
|
2152
2544
|
priceCents: tier.priceCents,
|
|
2153
2545
|
...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
|
|
2154
2546
|
...tier.blurb ? { blurb: tier.blurb } : {},
|
|
2155
|
-
sortOrder: tier.sortOrder ??
|
|
2547
|
+
sortOrder: tier.sortOrder ?? index2,
|
|
2156
2548
|
active: tier.active !== false,
|
|
2157
2549
|
createdAt: now
|
|
2158
2550
|
}));
|
|
@@ -2225,13 +2617,26 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2225
2617
|
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
2226
2618
|
const runbooks = isLeader ? {
|
|
2227
2619
|
role: "leader",
|
|
2228
|
-
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
2620
|
+
slugs: ["chapter-leader", "chapter-network", "crm", ...chapter.networkCommercialReadModel ? ["stripe-network-shadow"] : []],
|
|
2229
2621
|
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
2622
|
} : {
|
|
2231
2623
|
role: "follower",
|
|
2232
2624
|
slugs: ["chapter-follower", "chapter-network"],
|
|
2233
2625
|
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
2626
|
};
|
|
2627
|
+
const secrets = [];
|
|
2628
|
+
if (chapter.account !== "none") secrets.push({
|
|
2629
|
+
// Released Chapter workers request this effective name. odla-db maps it to
|
|
2630
|
+
// the canonical reserved `$clerk_secret` value written by the CLI.
|
|
2631
|
+
name: "clerk_secret_key",
|
|
2632
|
+
description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
|
|
2633
|
+
required: true
|
|
2634
|
+
});
|
|
2635
|
+
if (chapter.networkCommercialReadModel) secrets.push({
|
|
2636
|
+
name: "network_commercial_stripe_webhook_secret",
|
|
2637
|
+
description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
|
|
2638
|
+
required: true
|
|
2639
|
+
});
|
|
2235
2640
|
return {
|
|
2236
2641
|
id: "chapter",
|
|
2237
2642
|
title: `Chapter: ${chapter.name}`,
|
|
@@ -2242,14 +2647,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2242
2647
|
},
|
|
2243
2648
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
2244
2649
|
seeds,
|
|
2245
|
-
secrets
|
|
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
|
-
}],
|
|
2650
|
+
secrets,
|
|
2253
2651
|
probes: [
|
|
2254
2652
|
...crmDesc.probes ?? [],
|
|
2255
2653
|
// The snapshot route is deliberately private. A credential-free smoke
|
|
@@ -2272,7 +2670,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2272
2670
|
{ path: "/api/admin/builder-library", expectedStatus: 401 },
|
|
2273
2671
|
{ path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
|
|
2274
2672
|
] : [],
|
|
2275
|
-
...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : []
|
|
2673
|
+
...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : [],
|
|
2674
|
+
...chapter.networkCommercialReadModel ? [
|
|
2675
|
+
{ path: "/api/network-commercial/stripe-webhook", expectedStatus: 405 },
|
|
2676
|
+
{ path: "/api/admin/network-commercial-read-model", expectedStatus: 401 }
|
|
2677
|
+
] : []
|
|
2276
2678
|
],
|
|
2277
2679
|
runbooks
|
|
2278
2680
|
};
|
|
@@ -2280,7 +2682,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2280
2682
|
|
|
2281
2683
|
// src/member-content-validation-helpers.ts
|
|
2282
2684
|
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2283
|
-
var
|
|
2685
|
+
var SLUG8 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2284
2686
|
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2285
2687
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2286
2688
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
@@ -2312,21 +2714,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
|
2312
2714
|
return valueText;
|
|
2313
2715
|
}
|
|
2314
2716
|
function offerValue(value, path, errors, tiers2, audience) {
|
|
2315
|
-
const
|
|
2316
|
-
if (!
|
|
2717
|
+
const raw2 = record(value);
|
|
2718
|
+
if (!raw2) {
|
|
2317
2719
|
errors.push(`${path} must be an object`);
|
|
2318
2720
|
return void 0;
|
|
2319
2721
|
}
|
|
2320
|
-
unknownKeys(
|
|
2321
|
-
const tierId = textValue(
|
|
2722
|
+
unknownKeys(raw2, OFFER_KEYS, path, errors);
|
|
2723
|
+
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2322
2724
|
if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2323
2725
|
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2324
2726
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2325
2727
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
2326
2728
|
}
|
|
2327
|
-
const label = textValue(
|
|
2328
|
-
const bodyMarkdown = textValue(
|
|
2329
|
-
const ctaLabel = textValue(
|
|
2729
|
+
const label = textValue(raw2.label, `${path}.label`, errors);
|
|
2730
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2731
|
+
const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2330
2732
|
return {
|
|
2331
2733
|
tierId,
|
|
2332
2734
|
...label ? { label } : {},
|
|
@@ -2378,37 +2780,37 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2378
2780
|
const ids = /* @__PURE__ */ new Set();
|
|
2379
2781
|
const slugs = /* @__PURE__ */ new Set();
|
|
2380
2782
|
const pages = [];
|
|
2381
|
-
for (const [
|
|
2382
|
-
const path = `document.pages[${
|
|
2383
|
-
const
|
|
2384
|
-
if (!
|
|
2783
|
+
for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2784
|
+
const path = `document.pages[${index2}]`;
|
|
2785
|
+
const raw2 = record(value);
|
|
2786
|
+
if (!raw2) {
|
|
2385
2787
|
errors.push(`${path} must be an object`);
|
|
2386
2788
|
continue;
|
|
2387
2789
|
}
|
|
2388
|
-
unknownKeys(
|
|
2389
|
-
const
|
|
2390
|
-
const slug = textValue(
|
|
2391
|
-
const title = textValue(
|
|
2392
|
-
if (
|
|
2393
|
-
if (slug && !
|
|
2394
|
-
if (ids.has(
|
|
2790
|
+
unknownKeys(raw2, PAGE_KEYS, path, errors);
|
|
2791
|
+
const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2792
|
+
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2793
|
+
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2794
|
+
if (id6 && !ID.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2795
|
+
if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2796
|
+
if (ids.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
|
|
2395
2797
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2396
|
-
ids.add(
|
|
2798
|
+
ids.add(id6);
|
|
2397
2799
|
slugs.add(slug);
|
|
2398
|
-
const navRaw = record(
|
|
2399
|
-
let navigation = { section: "General", order:
|
|
2800
|
+
const navRaw = record(raw2.navigation);
|
|
2801
|
+
let navigation = { section: "General", order: index2 };
|
|
2400
2802
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2401
2803
|
else {
|
|
2402
2804
|
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2403
2805
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2404
|
-
const
|
|
2405
|
-
if (!Number.isInteger(
|
|
2806
|
+
const order2 = navRaw.order;
|
|
2807
|
+
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
2406
2808
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2407
2809
|
}
|
|
2408
2810
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2409
|
-
navigation = { section, order: Number.isInteger(
|
|
2811
|
+
navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
|
|
2410
2812
|
}
|
|
2411
|
-
const audienceRaw = record(
|
|
2813
|
+
const audienceRaw = record(raw2.audience);
|
|
2412
2814
|
let audience = { kind: "all-members" };
|
|
2413
2815
|
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2414
2816
|
else {
|
|
@@ -2430,12 +2832,12 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2430
2832
|
audience = { kind: "tiers", tierIds: normalized };
|
|
2431
2833
|
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2432
2834
|
}
|
|
2433
|
-
const summary = textValue(
|
|
2434
|
-
const teaserMarkdown = textValue(
|
|
2435
|
-
const bodyMarkdown = textValue(
|
|
2835
|
+
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
2836
|
+
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2837
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2436
2838
|
let upsell;
|
|
2437
|
-
if (
|
|
2438
|
-
const upsellRaw = record(
|
|
2839
|
+
if (raw2.upsell !== void 0) {
|
|
2840
|
+
const upsellRaw = record(raw2.upsell);
|
|
2439
2841
|
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2440
2842
|
else {
|
|
2441
2843
|
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
@@ -2449,13 +2851,13 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2449
2851
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2450
2852
|
}
|
|
2451
2853
|
}
|
|
2452
|
-
pages.push({ id:
|
|
2854
|
+
pages.push({ id: id6, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2453
2855
|
}
|
|
2454
2856
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2455
|
-
for (const [
|
|
2857
|
+
for (const [index2, page] of pages.entries()) {
|
|
2456
2858
|
const parentId = page.navigation.parentId;
|
|
2457
|
-
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${
|
|
2458
|
-
if (parentId === page.id) errors.push(`document.pages[${
|
|
2859
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
|
|
2860
|
+
if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
|
|
2459
2861
|
}
|
|
2460
2862
|
for (const page of pages) {
|
|
2461
2863
|
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
@@ -2476,21 +2878,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2476
2878
|
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2477
2879
|
const seen = /* @__PURE__ */ new Set();
|
|
2478
2880
|
upsellOverrides = [];
|
|
2479
|
-
for (const [
|
|
2480
|
-
const path = `document.upsellOverrides[${
|
|
2481
|
-
const
|
|
2482
|
-
if (!
|
|
2881
|
+
for (const [index2, value] of values.entries()) {
|
|
2882
|
+
const path = `document.upsellOverrides[${index2}]`;
|
|
2883
|
+
const raw2 = record(value);
|
|
2884
|
+
if (!raw2) {
|
|
2483
2885
|
errors.push(`${path} must be an object`);
|
|
2484
2886
|
continue;
|
|
2485
2887
|
}
|
|
2486
|
-
unknownKeys(
|
|
2487
|
-
const pageId = textValue(
|
|
2888
|
+
unknownKeys(raw2, OVERRIDE_KEYS, path, errors);
|
|
2889
|
+
const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2488
2890
|
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2489
2891
|
seen.add(pageId);
|
|
2490
2892
|
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2491
2893
|
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2492
2894
|
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2493
|
-
const offer = offerValue(
|
|
2895
|
+
const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2494
2896
|
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2495
2897
|
}
|
|
2496
2898
|
}
|
|
@@ -2615,7 +3017,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
2615
3017
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
2616
3018
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2617
3019
|
}
|
|
2618
|
-
function
|
|
3020
|
+
function text2(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
2619
3021
|
if (value === void 0 && !required) return void 0;
|
|
2620
3022
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
2621
3023
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -2625,23 +3027,23 @@ function text(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
|
2625
3027
|
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2626
3028
|
return result;
|
|
2627
3029
|
}
|
|
2628
|
-
function
|
|
3030
|
+
function integer2(value, path, errors, minimum = 1) {
|
|
2629
3031
|
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
2630
3032
|
return Number(value);
|
|
2631
3033
|
}
|
|
2632
3034
|
function scopeValue(value, path, errors) {
|
|
2633
|
-
const
|
|
2634
|
-
if (!
|
|
3035
|
+
const raw2 = record2(value);
|
|
3036
|
+
if (!raw2) {
|
|
2635
3037
|
errors.push(`${path} must be an object`);
|
|
2636
3038
|
return null;
|
|
2637
3039
|
}
|
|
2638
|
-
unknownKeys2(
|
|
2639
|
-
if (
|
|
2640
|
-
if (
|
|
2641
|
-
return { kind:
|
|
3040
|
+
unknownKeys2(raw2, SCOPE_KEYS, path, errors);
|
|
3041
|
+
if (raw2.kind === "global" || raw2.kind === "built_not_found") {
|
|
3042
|
+
if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
3043
|
+
return { kind: raw2.kind };
|
|
2642
3044
|
}
|
|
2643
|
-
if (
|
|
2644
|
-
const chapterId =
|
|
3045
|
+
if (raw2.kind === "chapter") {
|
|
3046
|
+
const chapterId = text2(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
2645
3047
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
2646
3048
|
return { kind: "chapter", chapterId };
|
|
2647
3049
|
}
|
|
@@ -2649,28 +3051,28 @@ function scopeValue(value, path, errors) {
|
|
|
2649
3051
|
return null;
|
|
2650
3052
|
}
|
|
2651
3053
|
function resourceValue(value, path, errors) {
|
|
2652
|
-
const
|
|
2653
|
-
if (!
|
|
3054
|
+
const raw2 = record2(value);
|
|
3055
|
+
if (!raw2) {
|
|
2654
3056
|
errors.push(`${path} must be an object`);
|
|
2655
3057
|
return null;
|
|
2656
3058
|
}
|
|
2657
|
-
unknownKeys2(
|
|
2658
|
-
const
|
|
2659
|
-
if (
|
|
2660
|
-
const kind =
|
|
3059
|
+
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
3060
|
+
const id6 = text2(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
3061
|
+
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
3062
|
+
const kind = text2(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
2661
3063
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
2662
|
-
const title =
|
|
2663
|
-
const summary =
|
|
2664
|
-
const status =
|
|
3064
|
+
const title = text2(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
3065
|
+
const summary = text2(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
3066
|
+
const status = text2(raw2.status, `${path}.status`, errors) ?? "";
|
|
2665
3067
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
2666
|
-
const revision =
|
|
2667
|
-
const scope = scopeValue(
|
|
2668
|
-
const tags = Array.isArray(
|
|
2669
|
-
if (!Array.isArray(
|
|
3068
|
+
const revision = integer2(raw2.revision, `${path}.revision`, errors);
|
|
3069
|
+
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
3070
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text2(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
3071
|
+
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
2670
3072
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
2671
3073
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
2672
|
-
const bodyMarkdown =
|
|
2673
|
-
const url =
|
|
3074
|
+
const bodyMarkdown = text2(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
3075
|
+
const url = text2(raw2.url, `${path}.url`, errors, 2048, false);
|
|
2674
3076
|
if (url) {
|
|
2675
3077
|
try {
|
|
2676
3078
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -2678,13 +3080,13 @@ function resourceValue(value, path, errors) {
|
|
|
2678
3080
|
errors.push(`${path}.url must be a valid https URL`);
|
|
2679
3081
|
}
|
|
2680
3082
|
}
|
|
2681
|
-
const asset =
|
|
2682
|
-
if (
|
|
3083
|
+
const asset = raw2.brandAsset === void 0 ? null : record2(raw2.brandAsset);
|
|
3084
|
+
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
2683
3085
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
2684
3086
|
const brandAsset = asset ? {
|
|
2685
|
-
bookId:
|
|
2686
|
-
assetId:
|
|
2687
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
3087
|
+
bookId: text2(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
3088
|
+
assetId: text2(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
3089
|
+
...asset.filename === void 0 ? {} : { filename: text2(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
2688
3090
|
} : void 0;
|
|
2689
3091
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
2690
3092
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -2692,59 +3094,59 @@ function resourceValue(value, path, errors) {
|
|
|
2692
3094
|
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
2693
3095
|
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
2694
3096
|
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
2695
|
-
const provenanceRaw = record2(
|
|
3097
|
+
const provenanceRaw = record2(raw2.provenance);
|
|
2696
3098
|
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
2697
3099
|
if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
2698
3100
|
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
|
|
2699
3101
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
2700
3102
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
2701
3103
|
const promotedFrom = promotedRaw ? {
|
|
2702
|
-
chapterId:
|
|
2703
|
-
resourceId:
|
|
2704
|
-
resourceRevision:
|
|
2705
|
-
documentRevision:
|
|
2706
|
-
promotedBy:
|
|
2707
|
-
promotedAt:
|
|
3104
|
+
chapterId: text2(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
3105
|
+
resourceId: text2(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
3106
|
+
resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
3107
|
+
documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
3108
|
+
promotedBy: text2(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
3109
|
+
promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
2708
3110
|
} : void 0;
|
|
2709
3111
|
const provenance = {
|
|
2710
|
-
createdBy:
|
|
2711
|
-
createdAt:
|
|
2712
|
-
updatedBy:
|
|
2713
|
-
updatedAt:
|
|
3112
|
+
createdBy: text2(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
3113
|
+
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
3114
|
+
updatedBy: text2(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
3115
|
+
updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
2714
3116
|
...promotedFrom ? { promotedFrom } : {}
|
|
2715
3117
|
};
|
|
2716
|
-
return { id:
|
|
3118
|
+
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
2717
3119
|
}
|
|
2718
3120
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
2719
3121
|
const errors = [];
|
|
2720
|
-
const
|
|
2721
|
-
if (!
|
|
2722
|
-
unknownKeys2(
|
|
2723
|
-
if (
|
|
2724
|
-
const revision =
|
|
2725
|
-
const generatedAt =
|
|
2726
|
-
const sourceRaw = record2(
|
|
3122
|
+
const raw2 = record2(value);
|
|
3123
|
+
if (!raw2) return { ok: false, errors: ["document must be an object"] };
|
|
3124
|
+
unknownKeys2(raw2, ROOT_KEYS2, "document", errors);
|
|
3125
|
+
if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
3126
|
+
const revision = integer2(raw2.revision, "document.revision", errors);
|
|
3127
|
+
const generatedAt = integer2(raw2.generatedAt, "document.generatedAt", errors, 0);
|
|
3128
|
+
const sourceRaw = record2(raw2.source);
|
|
2727
3129
|
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2728
3130
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
2729
3131
|
const sourceType = sourceRaw?.type;
|
|
2730
3132
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
2731
|
-
const sourceAppId =
|
|
3133
|
+
const sourceAppId = text2(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
2732
3134
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2733
3135
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2734
3136
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2735
|
-
const targetRaw =
|
|
2736
|
-
if (
|
|
3137
|
+
const targetRaw = raw2.target === void 0 ? null : record2(raw2.target);
|
|
3138
|
+
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
2737
3139
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
2738
3140
|
const target = targetRaw ? {
|
|
2739
|
-
appId:
|
|
2740
|
-
environment:
|
|
3141
|
+
appId: text2(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
3142
|
+
environment: text2(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
2741
3143
|
} : void 0;
|
|
2742
3144
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
2743
|
-
if (!Array.isArray(
|
|
2744
|
-
const inputs = Array.isArray(
|
|
3145
|
+
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
3146
|
+
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
2745
3147
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
2746
|
-
const resources = inputs.map((item,
|
|
2747
|
-
const ids = resources.map(({ id:
|
|
3148
|
+
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
3149
|
+
const ids = resources.map(({ id: id6 }) => id6);
|
|
2748
3150
|
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
2749
3151
|
for (const resource of resources) {
|
|
2750
3152
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -2861,10 +3263,10 @@ function planDelivery(input) {
|
|
|
2861
3263
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
2862
3264
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
2863
3265
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
2864
|
-
const
|
|
3266
|
+
const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
2865
3267
|
|
|
2866
3268
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
2867
|
-
return { deliver: true, transport, to, subject, text:
|
|
3269
|
+
return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
|
|
2868
3270
|
}
|
|
2869
3271
|
|
|
2870
3272
|
// src/notify.ts
|
|
@@ -2899,9 +3301,9 @@ async function sendTemplated(deps, input) {
|
|
|
2899
3301
|
error = e instanceof Error ? e.message : String(e);
|
|
2900
3302
|
}
|
|
2901
3303
|
}
|
|
2902
|
-
const
|
|
3304
|
+
const id6 = deps.newId();
|
|
2903
3305
|
const row = {
|
|
2904
|
-
id:
|
|
3306
|
+
id: id6,
|
|
2905
3307
|
groupId: input.group.id,
|
|
2906
3308
|
to: decision.to,
|
|
2907
3309
|
template: input.template,
|
|
@@ -2915,7 +3317,7 @@ async function sendTemplated(deps, input) {
|
|
|
2915
3317
|
...messageId ? { messageId } : {},
|
|
2916
3318
|
...error ? { error } : {}
|
|
2917
3319
|
};
|
|
2918
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
3320
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
2919
3321
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
2920
3322
|
}
|
|
2921
3323
|
function emailGroupFrom(row) {
|
|
@@ -3101,29 +3503,29 @@ function validateAdmissionGrantDocument(input) {
|
|
|
3101
3503
|
const grants = [];
|
|
3102
3504
|
const seenIds = /* @__PURE__ */ new Set();
|
|
3103
3505
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
3104
|
-
for (const [
|
|
3105
|
-
const grant =
|
|
3106
|
-
const
|
|
3506
|
+
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
3507
|
+
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
3508
|
+
const id6 = typeof grant.id === "string" ? grant.id : "";
|
|
3107
3509
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
3108
3510
|
const normalized = normalizeAdmissionEmail(email);
|
|
3109
3511
|
const decision = grant.decision;
|
|
3110
3512
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
3111
3513
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
3112
|
-
if (!ID3.test(
|
|
3113
|
-
if (seenIds.has(
|
|
3114
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${
|
|
3514
|
+
if (!ID3.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
3515
|
+
if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
|
|
3516
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
3115
3517
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
3116
3518
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
3117
|
-
errors.push(`document.grants[${
|
|
3519
|
+
errors.push(`document.grants[${index2}].decision is invalid`);
|
|
3118
3520
|
}
|
|
3119
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${
|
|
3521
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
|
|
3120
3522
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
3121
|
-
errors.push(`document.grants[${
|
|
3523
|
+
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
3122
3524
|
}
|
|
3123
|
-
seenIds.add(
|
|
3525
|
+
seenIds.add(id6);
|
|
3124
3526
|
seenEmails.add(normalized);
|
|
3125
3527
|
grants.push({
|
|
3126
|
-
id:
|
|
3528
|
+
id: id6,
|
|
3127
3529
|
email,
|
|
3128
3530
|
decision,
|
|
3129
3531
|
...note ? { note } : {},
|
|
@@ -3194,8 +3596,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3194
3596
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
3195
3597
|
const ops = [];
|
|
3196
3598
|
for (const row of current) {
|
|
3197
|
-
const
|
|
3198
|
-
if (
|
|
3599
|
+
const id6 = String(row.id ?? "");
|
|
3600
|
+
if (id6 && !incomingIds.has(id6)) ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
|
|
3199
3601
|
active: false,
|
|
3200
3602
|
revokedAt: receivedAt,
|
|
3201
3603
|
revokedBy: document.source.actor,
|
|
@@ -3204,10 +3606,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3204
3606
|
} });
|
|
3205
3607
|
}
|
|
3206
3608
|
for (const grant of document.grants) {
|
|
3207
|
-
const
|
|
3208
|
-
const existing = current.find((row) => row.id ===
|
|
3209
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
3210
|
-
id:
|
|
3609
|
+
const id6 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
3610
|
+
const existing = current.find((row) => row.id === id6);
|
|
3611
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
|
|
3612
|
+
id: id6,
|
|
3211
3613
|
sourceGrantId: grant.id,
|
|
3212
3614
|
sourceScope: "parent",
|
|
3213
3615
|
sourceId: document.source.appId,
|
|
@@ -3228,7 +3630,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3228
3630
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
3229
3631
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
3230
3632
|
];
|
|
3231
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
3633
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id6, attrs: retract });
|
|
3232
3634
|
}
|
|
3233
3635
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
3234
3636
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -3270,7 +3672,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
3270
3672
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
3271
3673
|
var CURRENCY = /^[a-z]{3}$/;
|
|
3272
3674
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3273
|
-
var
|
|
3675
|
+
var text3 = (value, path, errors, max) => {
|
|
3274
3676
|
if (typeof value !== "string" || !value.trim()) {
|
|
3275
3677
|
errors.push(`${path} is required`);
|
|
3276
3678
|
return "";
|
|
@@ -3289,32 +3691,32 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3289
3691
|
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
3290
3692
|
const ids = /* @__PURE__ */ new Set();
|
|
3291
3693
|
const offers = [];
|
|
3292
|
-
for (const [
|
|
3293
|
-
const path = `document.giftOffers.offers[${
|
|
3294
|
-
const item = record3(
|
|
3694
|
+
for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
3695
|
+
const path = `document.giftOffers.offers[${index2}]`;
|
|
3696
|
+
const item = record3(raw2);
|
|
3295
3697
|
if (!item) {
|
|
3296
3698
|
errors.push(`${path} must be an object`);
|
|
3297
3699
|
continue;
|
|
3298
3700
|
}
|
|
3299
3701
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
3300
3702
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3301
|
-
const
|
|
3302
|
-
if (
|
|
3303
|
-
if (ids.has(
|
|
3304
|
-
ids.add(
|
|
3703
|
+
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
3704
|
+
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3705
|
+
if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
3706
|
+
ids.add(id6);
|
|
3305
3707
|
const targetRaw = record3(item.target);
|
|
3306
3708
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
3307
3709
|
if (targetRaw?.type === "tier") {
|
|
3308
|
-
const tierId =
|
|
3710
|
+
const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
3309
3711
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
3310
3712
|
target = { type: "tier", tierId };
|
|
3311
3713
|
} else if (targetRaw?.type === "entitlement") {
|
|
3312
|
-
const entitlementKey =
|
|
3714
|
+
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
3313
3715
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
3314
3716
|
target = { type: "entitlement", entitlementKey };
|
|
3315
3717
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
3316
3718
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
3317
|
-
const stripePriceId =
|
|
3719
|
+
const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
3318
3720
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
3319
3721
|
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
3722
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -3327,7 +3729,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3327
3729
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
3328
3730
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
3329
3731
|
offers.push({
|
|
3330
|
-
id:
|
|
3732
|
+
id: id6,
|
|
3331
3733
|
target,
|
|
3332
3734
|
billingMode: item.billingMode,
|
|
3333
3735
|
stripePriceId,
|
|
@@ -3336,9 +3738,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3336
3738
|
redemptionLifetimeHours: lifetime,
|
|
3337
3739
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
3338
3740
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
3339
|
-
title:
|
|
3340
|
-
description:
|
|
3341
|
-
ctaLabel:
|
|
3741
|
+
title: text3(item.title, `${path}.title`, errors, 160),
|
|
3742
|
+
description: text3(item.description, `${path}.description`, errors, 2e3),
|
|
3743
|
+
ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
3342
3744
|
});
|
|
3343
3745
|
}
|
|
3344
3746
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -3383,8 +3785,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
3383
3785
|
const typeKey = type.toLowerCase();
|
|
3384
3786
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
3385
3787
|
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
3386
|
-
const
|
|
3387
|
-
if (readable &&
|
|
3788
|
+
const raw2 = `${prefix}:${hubRecordId}`;
|
|
3789
|
+
if (readable && raw2.length <= 64) return raw2;
|
|
3388
3790
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
3389
3791
|
}
|
|
3390
3792
|
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
@@ -3654,14 +4056,14 @@ function periodSeries(points, now, count, periodMs) {
|
|
|
3654
4056
|
for (const point of points) {
|
|
3655
4057
|
if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
|
|
3656
4058
|
if (point.t >= start && point.t <= now) {
|
|
3657
|
-
const
|
|
3658
|
-
current[
|
|
4059
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
|
|
4060
|
+
current[index2] = (current[index2] ?? 0) + point.v;
|
|
3659
4061
|
} else if (point.t >= previousStart && point.t < start) {
|
|
3660
|
-
const
|
|
3661
|
-
previous[
|
|
4062
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
|
|
4063
|
+
previous[index2] = (previous[index2] ?? 0) + point.v;
|
|
3662
4064
|
}
|
|
3663
4065
|
}
|
|
3664
|
-
const labels = current.map((_,
|
|
4066
|
+
const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
|
|
3665
4067
|
return { labels, current, previous };
|
|
3666
4068
|
}
|
|
3667
4069
|
function dashboardMetricData(points, now) {
|
|
@@ -3745,9 +4147,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
3745
4147
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
3746
4148
|
if (!found.ok) return false;
|
|
3747
4149
|
const users = await found.json().catch(() => null);
|
|
3748
|
-
const
|
|
3749
|
-
if (!
|
|
3750
|
-
return updateClerkUserMetadata(secretKey,
|
|
4150
|
+
const id6 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
4151
|
+
if (!id6) return false;
|
|
4152
|
+
return updateClerkUserMetadata(secretKey, id6, input, fetchImpl);
|
|
3751
4153
|
}
|
|
3752
4154
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
3753
4155
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -3810,8 +4212,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
3810
4212
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
3811
4213
|
return user ? toRecord(user) : null;
|
|
3812
4214
|
}
|
|
3813
|
-
async function clerkGetUser(secretKey,
|
|
3814
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
4215
|
+
async function clerkGetUser(secretKey, id6, fetchImpl = fetch) {
|
|
4216
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id6)}`, secretKey, fetchImpl).catch(() => null);
|
|
3815
4217
|
return data ? toRecord(data) : null;
|
|
3816
4218
|
}
|
|
3817
4219
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -3827,8 +4229,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
3827
4229
|
}
|
|
3828
4230
|
return out;
|
|
3829
4231
|
}
|
|
3830
|
-
async function clerkSetRole(secretKey,
|
|
3831
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
4232
|
+
async function clerkSetRole(secretKey, id6, role, fetchImpl = fetch) {
|
|
4233
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id6)}/metadata`, {
|
|
3832
4234
|
method: "PATCH",
|
|
3833
4235
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
3834
4236
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -4054,8 +4456,8 @@ function chapterBrandFromTokens(compiled, base = {}) {
|
|
|
4054
4456
|
function isReconcilable(meeting, now) {
|
|
4055
4457
|
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
4056
4458
|
}
|
|
4057
|
-
function reconcileMeetings(meetings2,
|
|
4058
|
-
const byEvent = new Map(
|
|
4459
|
+
function reconcileMeetings(meetings2, events2, now) {
|
|
4460
|
+
const byEvent = new Map(events2.map((e) => [e.eventId, e]));
|
|
4059
4461
|
const decisions = [];
|
|
4060
4462
|
for (const m of meetings2) {
|
|
4061
4463
|
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
@@ -4265,10 +4667,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
4265
4667
|
errors.push(`${path} must be a string`);
|
|
4266
4668
|
return void 0;
|
|
4267
4669
|
}
|
|
4268
|
-
const
|
|
4269
|
-
if (required && !
|
|
4270
|
-
if (
|
|
4271
|
-
return
|
|
4670
|
+
const text5 = value.trim();
|
|
4671
|
+
if (required && !text5) errors.push(`${path} is required`);
|
|
4672
|
+
if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
4673
|
+
return text5;
|
|
4272
4674
|
}
|
|
4273
4675
|
function validateMembershipPageDocument(input, options = {}) {
|
|
4274
4676
|
const errors = [];
|
|
@@ -4286,9 +4688,9 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4286
4688
|
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
4287
4689
|
const seen = /* @__PURE__ */ new Set();
|
|
4288
4690
|
const tiers2 = [];
|
|
4289
|
-
for (const [
|
|
4290
|
-
const path = `document.tiers[${
|
|
4291
|
-
const item = objectRecord(
|
|
4691
|
+
for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
4692
|
+
const path = `document.tiers[${index2}]`;
|
|
4693
|
+
const item = objectRecord(raw2);
|
|
4292
4694
|
if (!item) {
|
|
4293
4695
|
errors.push(`${path} must be an object`);
|
|
4294
4696
|
continue;
|
|
@@ -4317,8 +4719,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4317
4719
|
else {
|
|
4318
4720
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
4319
4721
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
4320
|
-
const
|
|
4321
|
-
return
|
|
4722
|
+
const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
4723
|
+
return text5 ? [text5] : [];
|
|
4322
4724
|
});
|
|
4323
4725
|
}
|
|
4324
4726
|
}
|
|
@@ -4394,7 +4796,7 @@ function membershipPageDocument(input, options = {}) {
|
|
|
4394
4796
|
}
|
|
4395
4797
|
|
|
4396
4798
|
// src/signup-control-validate.ts
|
|
4397
|
-
var
|
|
4799
|
+
var SLUG9 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4398
4800
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
4399
4801
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
4400
4802
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
@@ -4417,7 +4819,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
4417
4819
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
4418
4820
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
4419
4821
|
}
|
|
4420
|
-
function
|
|
4822
|
+
function text4(value, path, errors, max = 65536) {
|
|
4421
4823
|
if (typeof value !== "string" || !value.trim()) {
|
|
4422
4824
|
errors.push(`${path} is required`);
|
|
4423
4825
|
return "";
|
|
@@ -4432,19 +4834,19 @@ function tiersOf(value, errors) {
|
|
|
4432
4834
|
}
|
|
4433
4835
|
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
4434
4836
|
const seen = /* @__PURE__ */ new Set();
|
|
4435
|
-
return value.slice(0, 20).flatMap((
|
|
4436
|
-
const path = `document.tiers[${
|
|
4437
|
-
const item = recordOf(
|
|
4837
|
+
return value.slice(0, 20).flatMap((raw2, index2) => {
|
|
4838
|
+
const path = `document.tiers[${index2}]`;
|
|
4839
|
+
const item = recordOf(raw2);
|
|
4438
4840
|
if (!item) {
|
|
4439
4841
|
errors.push(`${path} must be an object`);
|
|
4440
4842
|
return [];
|
|
4441
4843
|
}
|
|
4442
4844
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
4443
|
-
const
|
|
4444
|
-
if (
|
|
4445
|
-
if (seen.has(
|
|
4446
|
-
seen.add(
|
|
4447
|
-
const name =
|
|
4845
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
4846
|
+
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
4847
|
+
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
4848
|
+
seen.add(id6);
|
|
4849
|
+
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
4448
4850
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
4449
4851
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
4450
4852
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -4456,9 +4858,9 @@ function tiersOf(value, errors) {
|
|
|
4456
4858
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
4457
4859
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
4458
4860
|
}
|
|
4459
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
4861
|
+
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
4460
4862
|
return [{
|
|
4461
|
-
id:
|
|
4863
|
+
id: id6,
|
|
4462
4864
|
name,
|
|
4463
4865
|
available: item.available === true,
|
|
4464
4866
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -4512,7 +4914,7 @@ function policyOf(value, errors) {
|
|
|
4512
4914
|
const result = {};
|
|
4513
4915
|
for (const key of allowed2) {
|
|
4514
4916
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
4515
|
-
result[key] =
|
|
4917
|
+
result[key] = text4(input[key], `document.policy.${key}`, errors);
|
|
4516
4918
|
}
|
|
4517
4919
|
return result;
|
|
4518
4920
|
}
|
|
@@ -4550,18 +4952,18 @@ function validateSignupControlDocument(input) {
|
|
|
4550
4952
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
4551
4953
|
const source = recordOf(root.source);
|
|
4552
4954
|
const target = recordOf(root.target);
|
|
4553
|
-
const appId =
|
|
4554
|
-
const chapterId =
|
|
4555
|
-
const environment =
|
|
4556
|
-
if (appId && !
|
|
4557
|
-
if (chapterId && !
|
|
4955
|
+
const appId = text4(source?.appId, "document.source.appId", errors, 63);
|
|
4956
|
+
const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
4957
|
+
const environment = text4(target?.environment, "document.target.environment", errors, 63);
|
|
4958
|
+
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
4959
|
+
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
4558
4960
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
4559
4961
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
4560
4962
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
4561
4963
|
const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
4562
4964
|
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
4563
4965
|
if (!page.ok) errors.push(...page.errors);
|
|
4564
|
-
else if (page.value.tiers.some((tier,
|
|
4966
|
+
else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
|
|
4565
4967
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
4566
4968
|
}
|
|
4567
4969
|
const application = applicationOf(root.application, errors);
|
|
@@ -4608,14 +5010,14 @@ function validatedTokens(input) {
|
|
|
4608
5010
|
}
|
|
4609
5011
|
const tokens = {};
|
|
4610
5012
|
let serializedLength = 0;
|
|
4611
|
-
for (const [name,
|
|
5013
|
+
for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
4612
5014
|
if (!TOKEN_NAME.test(name)) {
|
|
4613
5015
|
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
4614
5016
|
}
|
|
4615
|
-
if (typeof
|
|
5017
|
+
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
4616
5018
|
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
4617
5019
|
}
|
|
4618
|
-
const value = String(
|
|
5020
|
+
const value = String(raw2).trim();
|
|
4619
5021
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
4620
5022
|
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
4621
5023
|
}
|
|
@@ -4722,16 +5124,16 @@ function markdownSection(rawHeading) {
|
|
|
4722
5124
|
}
|
|
4723
5125
|
function markdownBenefits(lines, path, errors) {
|
|
4724
5126
|
const benefits = [];
|
|
4725
|
-
for (const [
|
|
5127
|
+
for (const [index2, line] of lines.entries()) {
|
|
4726
5128
|
if (!line.trim()) continue;
|
|
4727
5129
|
const match = line.match(MARKDOWN_BENEFIT);
|
|
4728
5130
|
if (!match) {
|
|
4729
|
-
errors.push(`${path} line ${
|
|
5131
|
+
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
4730
5132
|
continue;
|
|
4731
5133
|
}
|
|
4732
|
-
const
|
|
4733
|
-
if (
|
|
4734
|
-
if (
|
|
5134
|
+
const text5 = (match[1] ?? "").trim();
|
|
5135
|
+
if (text5.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
5136
|
+
if (text5) benefits.push(text5);
|
|
4735
5137
|
}
|
|
4736
5138
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
4737
5139
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -4746,7 +5148,7 @@ function applyMembershipTierMarkdown(presentation, source) {
|
|
|
4746
5148
|
const sections = /* @__PURE__ */ new Map();
|
|
4747
5149
|
const prelude = [];
|
|
4748
5150
|
let current = null;
|
|
4749
|
-
for (const [
|
|
5151
|
+
for (const [index2, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
4750
5152
|
const heading = line.match(MARKDOWN_SECTION);
|
|
4751
5153
|
if (heading) {
|
|
4752
5154
|
const headingText = heading[1] ?? "";
|
|
@@ -4766,7 +5168,7 @@ function applyMembershipTierMarkdown(presentation, source) {
|
|
|
4766
5168
|
continue;
|
|
4767
5169
|
}
|
|
4768
5170
|
if (MARKDOWN_HEADING.test(line)) {
|
|
4769
|
-
errors.push(`membership Markdown line ${
|
|
5171
|
+
errors.push(`membership Markdown line ${index2 + 1} uses an unsupported heading`);
|
|
4770
5172
|
continue;
|
|
4771
5173
|
}
|
|
4772
5174
|
if (current) sections.get(current)?.lines.push(line);
|
|
@@ -4833,7 +5235,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
4833
5235
|
id: chapterId,
|
|
4834
5236
|
reason: "provider_identity",
|
|
4835
5237
|
paths: [
|
|
4836
|
-
...paidTierIds.map((
|
|
5238
|
+
...paidTierIds.map((id6) => `tiers[${id6}].stripePriceId`),
|
|
4837
5239
|
"groups.stripePublishableKey",
|
|
4838
5240
|
"applications.stripeCustomerId",
|
|
4839
5241
|
"applications.stripeSubscriptionId",
|
|
@@ -4875,9 +5277,9 @@ async function exportChapterPromotion(input) {
|
|
|
4875
5277
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
4876
5278
|
receiptDigests: [input.digest]
|
|
4877
5279
|
};
|
|
4878
|
-
const artifact = (kind,
|
|
5280
|
+
const artifact = (kind, id6, content) => ({
|
|
4879
5281
|
kind,
|
|
4880
|
-
id:
|
|
5282
|
+
id: id6,
|
|
4881
5283
|
sourceRevision: revision,
|
|
4882
5284
|
provenance: promotionProvenance,
|
|
4883
5285
|
content: canonicalPromotionValue(content)
|
|
@@ -4916,7 +5318,7 @@ async function exportChapterPromotion(input) {
|
|
|
4916
5318
|
}
|
|
4917
5319
|
|
|
4918
5320
|
// src/member-system-authority.ts
|
|
4919
|
-
var rows2 = (group) => group.fields.map((
|
|
5321
|
+
var rows2 = (group) => group.fields.map((id6) => ({ ...group, id: id6 }));
|
|
4920
5322
|
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
4921
5323
|
...rows2({
|
|
4922
5324
|
scope: "network",
|
|
@@ -5301,10 +5703,10 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
5301
5703
|
if (supplied.length === keys.length) return "apply";
|
|
5302
5704
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
5303
5705
|
}
|
|
5304
|
-
function planMemberSystemEventReplay(
|
|
5706
|
+
function planMemberSystemEventReplay(events2) {
|
|
5305
5707
|
const seen = /* @__PURE__ */ new Map();
|
|
5306
5708
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
5307
|
-
for (const event of
|
|
5709
|
+
for (const event of events2) {
|
|
5308
5710
|
const digest = seen.get(event.id);
|
|
5309
5711
|
if (digest === void 0) {
|
|
5310
5712
|
seen.set(event.id, event.digest);
|
|
@@ -5459,6 +5861,7 @@ export {
|
|
|
5459
5861
|
joinConfig,
|
|
5460
5862
|
joinConfigTiers,
|
|
5461
5863
|
leaderCrmConfig,
|
|
5864
|
+
listStripeObjects,
|
|
5462
5865
|
loadLatestAdmissionRevision,
|
|
5463
5866
|
loadOfferableTiers,
|
|
5464
5867
|
loadTiers,
|
|
@@ -5473,9 +5876,11 @@ export {
|
|
|
5473
5876
|
memberSystemRollbackPlan,
|
|
5474
5877
|
membershipPageDocument,
|
|
5475
5878
|
membershipTierPresentationFromMarkdown,
|
|
5879
|
+
networkCommercialSourceKey,
|
|
5476
5880
|
networkSourceTag,
|
|
5477
5881
|
normalizeAdmissionEmail,
|
|
5478
5882
|
normalizeChapterEmail,
|
|
5883
|
+
normalizeNetworkCommercialStripeEvent,
|
|
5479
5884
|
normalizeSharedRecord,
|
|
5480
5885
|
normalizeWebhookEvent,
|
|
5481
5886
|
offerableTiers,
|
|
@@ -5488,10 +5893,12 @@ export {
|
|
|
5488
5893
|
projectBuilderLibraryDelivery,
|
|
5489
5894
|
projectMemberContent,
|
|
5490
5895
|
projectMemberSelf,
|
|
5896
|
+
projectNetworkCommercialEvents,
|
|
5491
5897
|
projectSharedRecord,
|
|
5492
5898
|
promoteBuilderLibraryResource,
|
|
5493
5899
|
publicGiftOffers,
|
|
5494
5900
|
reconcileMeetings,
|
|
5901
|
+
reconcileStripeSnapshot,
|
|
5495
5902
|
refundedPatch,
|
|
5496
5903
|
render,
|
|
5497
5904
|
renderSignupControlThemeHead,
|
|
@@ -5505,6 +5912,7 @@ export {
|
|
|
5505
5912
|
resolveJoinContinuation,
|
|
5506
5913
|
resolveLeaderFormation,
|
|
5507
5914
|
resolveMemberContent,
|
|
5915
|
+
resolveNetworkCommercialReadModel,
|
|
5508
5916
|
resolveNetworkPersonRegistry,
|
|
5509
5917
|
resolvePipeline,
|
|
5510
5918
|
resolveScheduling,
|