@odla-ai/chapter 0.43.2 → 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-Bh1EZvCg.d.ts → copy-context-CIxmneHI.d.ts} +42 -0
- package/dist/index.cjs +795 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -3
- package/dist/index.d.ts +155 -3
- package/dist/index.js +795 -275
- 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 +1476 -287
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +42 -0
- package/dist/worker/index.d.ts +42 -0
- package/dist/worker/index.js +1476 -287
- 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 {
|
|
@@ -466,6 +466,146 @@ var stripeEventReceiptsEntity = {
|
|
|
466
466
|
}
|
|
467
467
|
};
|
|
468
468
|
|
|
469
|
+
// src/schema-network-person.ts
|
|
470
|
+
var string = (flags = {}) => ({
|
|
471
|
+
type: "string",
|
|
472
|
+
unique: flags.unique ?? false,
|
|
473
|
+
indexed: flags.indexed ?? false,
|
|
474
|
+
optional: flags.optional ?? false
|
|
475
|
+
});
|
|
476
|
+
var number = (flags = {}) => ({
|
|
477
|
+
type: "number",
|
|
478
|
+
unique: false,
|
|
479
|
+
indexed: flags.indexed ?? false,
|
|
480
|
+
optional: flags.optional ?? false
|
|
481
|
+
});
|
|
482
|
+
var json = (flags = {}) => ({
|
|
483
|
+
type: "json",
|
|
484
|
+
unique: false,
|
|
485
|
+
indexed: false,
|
|
486
|
+
optional: flags.optional ?? false
|
|
487
|
+
});
|
|
488
|
+
var id3 = () => string({ unique: true, indexed: true });
|
|
489
|
+
var networkPersonsEntity = { attrs: {
|
|
490
|
+
id: id3(),
|
|
491
|
+
displayName: string({ indexed: true }),
|
|
492
|
+
state: string({ indexed: true }),
|
|
493
|
+
mergedIntoId: string({ indexed: true, optional: true }),
|
|
494
|
+
contactPolicy: json(),
|
|
495
|
+
revision: number({ indexed: true }),
|
|
496
|
+
createdAt: number({ indexed: true }),
|
|
497
|
+
updatedAt: number({ indexed: true })
|
|
498
|
+
} };
|
|
499
|
+
var networkIdentityClaimsEntity = { attrs: {
|
|
500
|
+
id: id3(),
|
|
501
|
+
personId: string({ indexed: true }),
|
|
502
|
+
kind: string({ indexed: true }),
|
|
503
|
+
value: string(),
|
|
504
|
+
hint: string({ optional: true }),
|
|
505
|
+
verifiedAt: number({ indexed: true }),
|
|
506
|
+
createdAt: number({ indexed: true })
|
|
507
|
+
} };
|
|
508
|
+
var networkPersonAliasesEntity = { attrs: {
|
|
509
|
+
id: id3(),
|
|
510
|
+
sourceSystem: string({ indexed: true }),
|
|
511
|
+
sourceRecordId: string({ indexed: true }),
|
|
512
|
+
sourceKey: string({ unique: true, indexed: true }),
|
|
513
|
+
displayName: string({ indexed: true }),
|
|
514
|
+
provenance: string(),
|
|
515
|
+
consent: json(),
|
|
516
|
+
status: string({ indexed: true }),
|
|
517
|
+
personId: string({ indexed: true, optional: true }),
|
|
518
|
+
candidatePersonIds: json({ optional: true }),
|
|
519
|
+
identityClaims: json(),
|
|
520
|
+
chapterLinks: json(),
|
|
521
|
+
revision: number({ indexed: true }),
|
|
522
|
+
createdAt: number({ indexed: true }),
|
|
523
|
+
updatedAt: number({ indexed: true })
|
|
524
|
+
} };
|
|
525
|
+
var networkChapterPersonLinksEntity = { attrs: {
|
|
526
|
+
id: id3(),
|
|
527
|
+
personId: string({ indexed: true }),
|
|
528
|
+
chapterId: string({ indexed: true }),
|
|
529
|
+
chapterPersonId: string({ indexed: true }),
|
|
530
|
+
state: string({ indexed: true }),
|
|
531
|
+
createdAt: number({ indexed: true })
|
|
532
|
+
} };
|
|
533
|
+
var networkPersonEventsEntity = { attrs: {
|
|
534
|
+
id: id3(),
|
|
535
|
+
kind: string({ indexed: true }),
|
|
536
|
+
personId: string({ indexed: true, optional: true }),
|
|
537
|
+
aliasId: string({ indexed: true, optional: true }),
|
|
538
|
+
actorId: string({ indexed: true }),
|
|
539
|
+
mutationId: string({ unique: true, indexed: true }),
|
|
540
|
+
detail: json(),
|
|
541
|
+
createdAt: number({ indexed: true })
|
|
542
|
+
} };
|
|
543
|
+
var networkPersonEntities = {
|
|
544
|
+
networkPersons: networkPersonsEntity,
|
|
545
|
+
networkIdentityClaims: networkIdentityClaimsEntity,
|
|
546
|
+
networkPersonAliases: networkPersonAliasesEntity,
|
|
547
|
+
networkChapterPersonLinks: networkChapterPersonLinksEntity,
|
|
548
|
+
networkPersonEvents: networkPersonEventsEntity
|
|
549
|
+
};
|
|
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
|
+
|
|
469
609
|
// src/schema.ts
|
|
470
610
|
function attr5(type, flags = {}) {
|
|
471
611
|
return {
|
|
@@ -475,10 +615,10 @@ function attr5(type, flags = {}) {
|
|
|
475
615
|
optional: flags.optional ?? false
|
|
476
616
|
};
|
|
477
617
|
}
|
|
478
|
-
var
|
|
618
|
+
var id5 = () => attr5("string", { unique: true, indexed: true });
|
|
479
619
|
var admins = {
|
|
480
620
|
attrs: {
|
|
481
|
-
id:
|
|
621
|
+
id: id5(),
|
|
482
622
|
email: attr5("string", { unique: true, indexed: true }),
|
|
483
623
|
name: attr5("string", { optional: true }),
|
|
484
624
|
note: attr5("string", { optional: true })
|
|
@@ -486,7 +626,7 @@ var admins = {
|
|
|
486
626
|
};
|
|
487
627
|
var superAdmins = {
|
|
488
628
|
attrs: {
|
|
489
|
-
id:
|
|
629
|
+
id: id5(),
|
|
490
630
|
email: attr5("string", { unique: true, indexed: true }),
|
|
491
631
|
note: attr5("string", { optional: true }),
|
|
492
632
|
createdAt: attr5("number", { indexed: true })
|
|
@@ -494,7 +634,7 @@ var superAdmins = {
|
|
|
494
634
|
};
|
|
495
635
|
var networkNotes = {
|
|
496
636
|
attrs: {
|
|
497
|
-
id:
|
|
637
|
+
id: id5(),
|
|
498
638
|
recordKey: attr5("string", { indexed: true }),
|
|
499
639
|
targetId: attr5("string", { indexed: true }),
|
|
500
640
|
recordType: attr5("string", { indexed: true }),
|
|
@@ -507,7 +647,7 @@ var networkNotes = {
|
|
|
507
647
|
};
|
|
508
648
|
var signupControls = {
|
|
509
649
|
attrs: {
|
|
510
|
-
id:
|
|
650
|
+
id: id5(),
|
|
511
651
|
sourceId: attr5("string", { indexed: true }),
|
|
512
652
|
targetId: attr5("string", { indexed: true }),
|
|
513
653
|
environment: attr5("string", { indexed: true }),
|
|
@@ -520,7 +660,7 @@ var signupControls = {
|
|
|
520
660
|
};
|
|
521
661
|
var memberContentControls = {
|
|
522
662
|
attrs: {
|
|
523
|
-
id:
|
|
663
|
+
id: id5(),
|
|
524
664
|
sourceId: attr5("string", { indexed: true }),
|
|
525
665
|
targetId: attr5("string", { indexed: true }),
|
|
526
666
|
environment: attr5("string", { indexed: true }),
|
|
@@ -533,7 +673,7 @@ var memberContentControls = {
|
|
|
533
673
|
};
|
|
534
674
|
var memberContentLocal = {
|
|
535
675
|
attrs: {
|
|
536
|
-
id:
|
|
676
|
+
id: id5(),
|
|
537
677
|
chapterId: attr5("string", { indexed: true }),
|
|
538
678
|
environment: attr5("string", { indexed: true }),
|
|
539
679
|
revision: attr5("number", { indexed: true }),
|
|
@@ -544,7 +684,7 @@ var memberContentLocal = {
|
|
|
544
684
|
};
|
|
545
685
|
var builderLibraryControls = {
|
|
546
686
|
attrs: {
|
|
547
|
-
id:
|
|
687
|
+
id: id5(),
|
|
548
688
|
sourceId: attr5("string", { indexed: true }),
|
|
549
689
|
targetId: attr5("string", { indexed: true }),
|
|
550
690
|
environment: attr5("string", { indexed: true }),
|
|
@@ -557,7 +697,7 @@ var builderLibraryControls = {
|
|
|
557
697
|
};
|
|
558
698
|
var builderLibraryLocal = {
|
|
559
699
|
attrs: {
|
|
560
|
-
id:
|
|
700
|
+
id: id5(),
|
|
561
701
|
appId: attr5("string", { indexed: true }),
|
|
562
702
|
environment: attr5("string", { indexed: true }),
|
|
563
703
|
revision: attr5("number", { indexed: true }),
|
|
@@ -568,7 +708,7 @@ var builderLibraryLocal = {
|
|
|
568
708
|
};
|
|
569
709
|
var groups = {
|
|
570
710
|
attrs: {
|
|
571
|
-
id:
|
|
711
|
+
id: id5(),
|
|
572
712
|
name: attr5("string"),
|
|
573
713
|
standardPriceCents: attr5("number"),
|
|
574
714
|
foundingDiscountCents: attr5("number"),
|
|
@@ -591,7 +731,7 @@ var groups = {
|
|
|
591
731
|
};
|
|
592
732
|
var tiers = {
|
|
593
733
|
attrs: {
|
|
594
|
-
id:
|
|
734
|
+
id: id5(),
|
|
595
735
|
groupId: attr5("string", { indexed: true }),
|
|
596
736
|
name: attr5("string"),
|
|
597
737
|
priceCents: attr5("number"),
|
|
@@ -604,7 +744,7 @@ var tiers = {
|
|
|
604
744
|
};
|
|
605
745
|
var meetings = {
|
|
606
746
|
attrs: {
|
|
607
|
-
id:
|
|
747
|
+
id: id5(),
|
|
608
748
|
applicationId: attr5("string", { indexed: true }),
|
|
609
749
|
groupId: attr5("string", { indexed: true }),
|
|
610
750
|
startAt: attr5("number", { indexed: true }),
|
|
@@ -623,7 +763,7 @@ var meetings = {
|
|
|
623
763
|
};
|
|
624
764
|
var emailLog = {
|
|
625
765
|
attrs: {
|
|
626
|
-
id:
|
|
766
|
+
id: id5(),
|
|
627
767
|
groupId: attr5("string", { indexed: true }),
|
|
628
768
|
applicationId: attr5("string", { indexed: true, optional: true }),
|
|
629
769
|
to: attr5("string", { indexed: true }),
|
|
@@ -638,7 +778,7 @@ var emailLog = {
|
|
|
638
778
|
sentAt: attr5("number", { indexed: true })
|
|
639
779
|
}
|
|
640
780
|
};
|
|
641
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false) {
|
|
781
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false, includeNetworkCommercialReadModel = false) {
|
|
642
782
|
const entities = {};
|
|
643
783
|
if (mode === "chapter") {
|
|
644
784
|
entities.applications = applicationsFor(application);
|
|
@@ -672,6 +812,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
672
812
|
entities.leaderAdmissionDeliveries = leaderAdmissionDeliveriesEntity;
|
|
673
813
|
entities.leaderAdmissionMutations = leaderAdmissionMutationsEntity;
|
|
674
814
|
}
|
|
815
|
+
if (includeNetworkPersonRegistry) {
|
|
816
|
+
Object.assign(entities, networkPersonEntities);
|
|
817
|
+
}
|
|
818
|
+
if (includeNetworkCommercialReadModel) Object.assign(entities, networkCommercialEntities);
|
|
675
819
|
const schema = { entities, links: {} };
|
|
676
820
|
const rules = {};
|
|
677
821
|
for (const ns of Object.keys(entities)) {
|
|
@@ -843,8 +987,8 @@ function resolveAuth(mode, auth) {
|
|
|
843
987
|
return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
|
|
844
988
|
}
|
|
845
989
|
function roleFromClaim(payload, auth) {
|
|
846
|
-
const
|
|
847
|
-
return typeof
|
|
990
|
+
const raw2 = payload[auth.claim];
|
|
991
|
+
return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
|
|
848
992
|
}
|
|
849
993
|
function isAdminRole(role, auth) {
|
|
850
994
|
return role === auth.adminRole;
|
|
@@ -1323,9 +1467,9 @@ var APPLICATION_LABELS = {
|
|
|
1323
1467
|
phone: "Phone",
|
|
1324
1468
|
state: "State"
|
|
1325
1469
|
};
|
|
1326
|
-
function humanize(
|
|
1327
|
-
const
|
|
1328
|
-
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;
|
|
1329
1473
|
}
|
|
1330
1474
|
function applicationDefs(chapter) {
|
|
1331
1475
|
try {
|
|
@@ -1334,23 +1478,23 @@ function applicationDefs(chapter) {
|
|
|
1334
1478
|
return {};
|
|
1335
1479
|
}
|
|
1336
1480
|
}
|
|
1337
|
-
function applicationType(
|
|
1481
|
+
function applicationType(id6, def) {
|
|
1338
1482
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1339
|
-
return
|
|
1483
|
+
return id6 === "email" ? "email" : id6 === "linkedin" ? "url" : "string";
|
|
1340
1484
|
}
|
|
1341
1485
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1342
1486
|
const defs = applicationDefs(chapter);
|
|
1343
|
-
return [...application.required, ...application.optional].map((
|
|
1344
|
-
const def = defs[
|
|
1345
|
-
const conditions = application.conditions[
|
|
1346
|
-
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);
|
|
1347
1491
|
return {
|
|
1348
|
-
id:
|
|
1349
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
1492
|
+
id: id6,
|
|
1493
|
+
label: def?.label ?? APPLICATION_LABELS[id6] ?? humanize(id6),
|
|
1350
1494
|
type,
|
|
1351
|
-
required: application.required.includes(
|
|
1352
|
-
order,
|
|
1353
|
-
maxLength: application.maxLen[
|
|
1495
|
+
required: application.required.includes(id6),
|
|
1496
|
+
order: order2,
|
|
1497
|
+
maxLength: application.maxLen[id6] ?? application.defaultMaxLen,
|
|
1354
1498
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
1355
1499
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
1356
1500
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -1360,17 +1504,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
1360
1504
|
function resolveFormationFlowFields(crm, formation) {
|
|
1361
1505
|
if (!formation.enabled) return [];
|
|
1362
1506
|
const def = crm.type(formation.type);
|
|
1363
|
-
return [...formation.required, ...formation.optional].map((
|
|
1364
|
-
const field = def.fields[
|
|
1507
|
+
return [...formation.required, ...formation.optional].map((id6, order2) => {
|
|
1508
|
+
const field = def.fields[id6];
|
|
1365
1509
|
return {
|
|
1366
|
-
id:
|
|
1367
|
-
label: field.label ?? humanize(
|
|
1510
|
+
id: id6,
|
|
1511
|
+
label: field.label ?? humanize(id6),
|
|
1368
1512
|
type: field.type,
|
|
1369
|
-
required: formation.required.includes(
|
|
1370
|
-
order,
|
|
1371
|
-
maxLength: formation.maxLen[
|
|
1513
|
+
required: formation.required.includes(id6),
|
|
1514
|
+
order: order2,
|
|
1515
|
+
maxLength: formation.maxLen[id6] ?? formation.defaultMaxLen,
|
|
1372
1516
|
...field.options ? { options: [...field.options] } : {},
|
|
1373
|
-
...
|
|
1517
|
+
...id6 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1374
1518
|
};
|
|
1375
1519
|
});
|
|
1376
1520
|
}
|
|
@@ -1410,7 +1554,7 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1410
1554
|
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
1411
1555
|
}
|
|
1412
1556
|
}
|
|
1413
|
-
const duplicates = [...required, ...optional].filter((field,
|
|
1557
|
+
const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
1414
1558
|
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
1415
1559
|
const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
|
|
1416
1560
|
if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
|
|
@@ -1461,7 +1605,7 @@ var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
|
1461
1605
|
var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
|
|
1462
1606
|
var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
1463
1607
|
function validateSharedNotes(opts) {
|
|
1464
|
-
const duplicate = opts.values.some((type,
|
|
1608
|
+
const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
|
|
1465
1609
|
const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type] || (opts.fields ? !opts.fields[type] : false));
|
|
1466
1610
|
if (duplicate || invalid) {
|
|
1467
1611
|
throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
|
|
@@ -1471,7 +1615,7 @@ function validateEditableFields(opts) {
|
|
|
1471
1615
|
for (const [type, names] of Object.entries(opts.values)) {
|
|
1472
1616
|
const readable = opts.readable?.[type];
|
|
1473
1617
|
const def = opts.crm.config.types[type];
|
|
1474
|
-
const duplicate = names.some((name,
|
|
1618
|
+
const duplicate = names.some((name, index2) => names.indexOf(name) !== index2);
|
|
1475
1619
|
const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
|
|
1476
1620
|
const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
|
|
1477
1621
|
if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
|
|
@@ -1482,7 +1626,7 @@ function validateEditableFields(opts) {
|
|
|
1482
1626
|
}
|
|
1483
1627
|
}
|
|
1484
1628
|
function validateStageTransitions(opts) {
|
|
1485
|
-
const duplicate = opts.values.some((type,
|
|
1629
|
+
const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
|
|
1486
1630
|
const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
|
|
1487
1631
|
if (duplicate || invalid) {
|
|
1488
1632
|
throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
|
|
@@ -1664,12 +1808,58 @@ function resolveNetwork(config, crm) {
|
|
|
1664
1808
|
return { targets, readers };
|
|
1665
1809
|
}
|
|
1666
1810
|
|
|
1811
|
+
// src/config-network-person.ts
|
|
1812
|
+
var SLUG2 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
1813
|
+
function resolveNetworkPersonRegistry(mode, input, targetIds) {
|
|
1814
|
+
if (!input) return null;
|
|
1815
|
+
if (mode !== "hub") throw new Error("defineChapter.networkPersonRegistry: only hub mode may own network Person authority");
|
|
1816
|
+
if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
|
|
1817
|
+
const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
|
|
1818
|
+
if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
|
|
1819
|
+
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id6) => !SLUG2.test(id6))) {
|
|
1820
|
+
throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
|
|
1821
|
+
}
|
|
1822
|
+
const unknown = chapterIds.filter((id6) => !targetIds.includes(id6));
|
|
1823
|
+
if (unknown.length) {
|
|
1824
|
+
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id6) => `"${id6}"`).join(", ")} are not configured network targets`);
|
|
1825
|
+
}
|
|
1826
|
+
return { mode: "shadow", chapterIds };
|
|
1827
|
+
}
|
|
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
|
+
|
|
1667
1857
|
// src/config-signup-control.ts
|
|
1668
|
-
var
|
|
1858
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1669
1859
|
var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1670
1860
|
function resolveSignupControl(input) {
|
|
1671
1861
|
if (!input) return null;
|
|
1672
|
-
if (!
|
|
1862
|
+
if (!SLUG4.test(input.sourceId)) {
|
|
1673
1863
|
throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
|
|
1674
1864
|
}
|
|
1675
1865
|
if (input.stripeMode !== "test" && input.stripeMode !== "live") {
|
|
@@ -1683,12 +1873,12 @@ function resolveSignupControl(input) {
|
|
|
1683
1873
|
}
|
|
1684
1874
|
|
|
1685
1875
|
// src/config-member-content.ts
|
|
1686
|
-
var
|
|
1876
|
+
var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1687
1877
|
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1688
1878
|
function resolveMemberContent(input) {
|
|
1689
1879
|
if (!input) return null;
|
|
1690
1880
|
const parent = input.parent;
|
|
1691
|
-
if (parent && !
|
|
1881
|
+
if (parent && !SLUG5.test(parent.sourceId)) {
|
|
1692
1882
|
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1693
1883
|
}
|
|
1694
1884
|
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
@@ -1703,22 +1893,22 @@ function resolveMemberContent(input) {
|
|
|
1703
1893
|
}
|
|
1704
1894
|
|
|
1705
1895
|
// src/config-builder-library.ts
|
|
1706
|
-
var
|
|
1896
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1707
1897
|
function resolveBuilderLibrary(mode, input, network = { targets: [], readers: [] }) {
|
|
1708
1898
|
if (!input) return null;
|
|
1709
1899
|
if (input.sources !== void 0 && !Array.isArray(input.sources)) {
|
|
1710
1900
|
throw new Error("defineChapter.builderLibrary.sources: must be an array");
|
|
1711
1901
|
}
|
|
1712
1902
|
const seen = /* @__PURE__ */ new Set();
|
|
1713
|
-
const sources = (input.sources ?? []).map((source,
|
|
1714
|
-
if (!source || typeof source !== "object" || !
|
|
1715
|
-
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`);
|
|
1716
1906
|
}
|
|
1717
1907
|
if (source.sourceId === "built-not-found" && mode === "hub") {
|
|
1718
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
1908
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}]: a hub accepts Chapter sources, not itself`);
|
|
1719
1909
|
}
|
|
1720
1910
|
if (mode === "chapter" && source.sourceId !== "built-not-found") {
|
|
1721
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
1911
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}]: Chapters may accept only the built-not-found source`);
|
|
1722
1912
|
}
|
|
1723
1913
|
if (seen.has(source.sourceId)) {
|
|
1724
1914
|
throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
|
|
@@ -1726,14 +1916,14 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
|
|
|
1726
1916
|
seen.add(source.sourceId);
|
|
1727
1917
|
if ("secretName" in source) {
|
|
1728
1918
|
throw new Error(
|
|
1729
|
-
`defineChapter.builderLibrary.sources[${
|
|
1919
|
+
`defineChapter.builderLibrary.sources[${index2}].secretName: remove this field; Builder Library reuses the Chapter network edge`
|
|
1730
1920
|
);
|
|
1731
1921
|
}
|
|
1732
1922
|
const networkPeer = mode === "hub" ? network.targets.some((target) => target.id === source.sourceId) : network.readers.some((reader) => reader.id === source.sourceId);
|
|
1733
1923
|
if (!networkPeer) {
|
|
1734
1924
|
const side = mode === "hub" ? "targets" : "readers";
|
|
1735
1925
|
throw new Error(
|
|
1736
|
-
`defineChapter.builderLibrary.sources[${
|
|
1926
|
+
`defineChapter.builderLibrary.sources[${index2}]: "${source.sourceId}" must already be configured in network.${side}`
|
|
1737
1927
|
);
|
|
1738
1928
|
}
|
|
1739
1929
|
return { sourceId: source.sourceId };
|
|
@@ -1742,15 +1932,15 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
|
|
|
1742
1932
|
}
|
|
1743
1933
|
|
|
1744
1934
|
// src/config.ts
|
|
1745
|
-
var
|
|
1935
|
+
var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1746
1936
|
function isResolvedCrm(x) {
|
|
1747
1937
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1748
1938
|
}
|
|
1749
1939
|
function defineChapter(config) {
|
|
1750
1940
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1751
|
-
const { id:
|
|
1752
|
-
if (typeof
|
|
1753
|
-
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)}`);
|
|
1754
1944
|
}
|
|
1755
1945
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1756
1946
|
const mode = config.mode ?? "chapter";
|
|
@@ -1773,6 +1963,16 @@ function defineChapter(config) {
|
|
|
1773
1963
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
1774
1964
|
const copy = resolveChapterCopy(config.copy);
|
|
1775
1965
|
const network = resolveNetwork(config, crm);
|
|
1966
|
+
const networkPersonRegistry = resolveNetworkPersonRegistry(
|
|
1967
|
+
mode,
|
|
1968
|
+
config.networkPersonRegistry,
|
|
1969
|
+
network.targets.map((target) => target.id)
|
|
1970
|
+
);
|
|
1971
|
+
const networkCommercialReadModel = resolveNetworkCommercialReadModel(
|
|
1972
|
+
mode,
|
|
1973
|
+
config.networkCommercialReadModel,
|
|
1974
|
+
network.targets.map((target) => target.id)
|
|
1975
|
+
);
|
|
1776
1976
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1777
1977
|
const memberContent = resolveMemberContent(config.memberContent);
|
|
1778
1978
|
const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary, network);
|
|
@@ -1791,7 +1991,9 @@ function defineChapter(config) {
|
|
|
1791
1991
|
Boolean(signupControl),
|
|
1792
1992
|
Boolean(memberContent),
|
|
1793
1993
|
Boolean(builderLibrary),
|
|
1794
|
-
network.targets.some((target) => target.admissionGrants)
|
|
1994
|
+
network.targets.some((target) => target.admissionGrants),
|
|
1995
|
+
Boolean(networkPersonRegistry),
|
|
1996
|
+
Boolean(networkCommercialReadModel)
|
|
1795
1997
|
);
|
|
1796
1998
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1797
1999
|
const account = config.account ?? "none";
|
|
@@ -1841,7 +2043,7 @@ function defineChapter(config) {
|
|
|
1841
2043
|
};
|
|
1842
2044
|
const chapter = {
|
|
1843
2045
|
config,
|
|
1844
|
-
id:
|
|
2046
|
+
id: id6,
|
|
1845
2047
|
name,
|
|
1846
2048
|
mode,
|
|
1847
2049
|
brand,
|
|
@@ -1850,6 +2052,8 @@ function defineChapter(config) {
|
|
|
1850
2052
|
signupControl,
|
|
1851
2053
|
memberContent,
|
|
1852
2054
|
builderLibrary,
|
|
2055
|
+
networkPersonRegistry,
|
|
2056
|
+
networkCommercialReadModel,
|
|
1853
2057
|
formation,
|
|
1854
2058
|
crm,
|
|
1855
2059
|
auth,
|
|
@@ -1867,6 +2071,304 @@ function defineChapter(config) {
|
|
|
1867
2071
|
return chapter;
|
|
1868
2072
|
}
|
|
1869
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
|
+
|
|
1870
2372
|
// src/leader.ts
|
|
1871
2373
|
import { composeCrmConfigs } from "@odla-ai/crm";
|
|
1872
2374
|
function leaderCrmConfig(options = {}) {
|
|
@@ -2035,14 +2537,14 @@ import { createCrmIntegration } from "@odla-ai/crm";
|
|
|
2035
2537
|
|
|
2036
2538
|
// src/tiers.ts
|
|
2037
2539
|
function buildTierSeeds(groupId, configured = [], now = 0) {
|
|
2038
|
-
return configured.map((tier,
|
|
2540
|
+
return configured.map((tier, index2) => ({
|
|
2039
2541
|
id: tier.id,
|
|
2040
2542
|
groupId,
|
|
2041
2543
|
name: tier.name,
|
|
2042
2544
|
priceCents: tier.priceCents,
|
|
2043
2545
|
...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
|
|
2044
2546
|
...tier.blurb ? { blurb: tier.blurb } : {},
|
|
2045
|
-
sortOrder: tier.sortOrder ??
|
|
2547
|
+
sortOrder: tier.sortOrder ?? index2,
|
|
2046
2548
|
active: tier.active !== false,
|
|
2047
2549
|
createdAt: now
|
|
2048
2550
|
}));
|
|
@@ -2115,13 +2617,26 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2115
2617
|
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
2116
2618
|
const runbooks = isLeader ? {
|
|
2117
2619
|
role: "leader",
|
|
2118
|
-
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
2620
|
+
slugs: ["chapter-leader", "chapter-network", "crm", ...chapter.networkCommercialReadModel ? ["stripe-network-shadow"] : []],
|
|
2119
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\`.`
|
|
2120
2622
|
} : {
|
|
2121
2623
|
role: "follower",
|
|
2122
2624
|
slugs: ["chapter-follower", "chapter-network"],
|
|
2123
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\`.`
|
|
2124
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
|
+
});
|
|
2125
2640
|
return {
|
|
2126
2641
|
id: "chapter",
|
|
2127
2642
|
title: `Chapter: ${chapter.name}`,
|
|
@@ -2132,14 +2647,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2132
2647
|
},
|
|
2133
2648
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
2134
2649
|
seeds,
|
|
2135
|
-
secrets
|
|
2136
|
-
// Released Chapter workers request this effective name. odla-db maps
|
|
2137
|
-
// it to the canonical reserved `$clerk_secret` value written by
|
|
2138
|
-
// `odla-ai secrets set-clerk-key`.
|
|
2139
|
-
name: "clerk_secret_key",
|
|
2140
|
-
description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
|
|
2141
|
-
required: true
|
|
2142
|
-
}],
|
|
2650
|
+
secrets,
|
|
2143
2651
|
probes: [
|
|
2144
2652
|
...crmDesc.probes ?? [],
|
|
2145
2653
|
// The snapshot route is deliberately private. A credential-free smoke
|
|
@@ -2161,6 +2669,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2161
2669
|
{ path: "/api/builder-library-control", expectedStatus: 401 },
|
|
2162
2670
|
{ path: "/api/admin/builder-library", expectedStatus: 401 },
|
|
2163
2671
|
{ path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
|
|
2672
|
+
] : [],
|
|
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 }
|
|
2164
2677
|
] : []
|
|
2165
2678
|
],
|
|
2166
2679
|
runbooks
|
|
@@ -2169,7 +2682,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2169
2682
|
|
|
2170
2683
|
// src/member-content-validation-helpers.ts
|
|
2171
2684
|
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2172
|
-
var
|
|
2685
|
+
var SLUG8 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2173
2686
|
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2174
2687
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2175
2688
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
@@ -2201,21 +2714,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
|
2201
2714
|
return valueText;
|
|
2202
2715
|
}
|
|
2203
2716
|
function offerValue(value, path, errors, tiers2, audience) {
|
|
2204
|
-
const
|
|
2205
|
-
if (!
|
|
2717
|
+
const raw2 = record(value);
|
|
2718
|
+
if (!raw2) {
|
|
2206
2719
|
errors.push(`${path} must be an object`);
|
|
2207
2720
|
return void 0;
|
|
2208
2721
|
}
|
|
2209
|
-
unknownKeys(
|
|
2210
|
-
const tierId = textValue(
|
|
2722
|
+
unknownKeys(raw2, OFFER_KEYS, path, errors);
|
|
2723
|
+
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2211
2724
|
if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2212
2725
|
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2213
2726
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2214
2727
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
2215
2728
|
}
|
|
2216
|
-
const label = textValue(
|
|
2217
|
-
const bodyMarkdown = textValue(
|
|
2218
|
-
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);
|
|
2219
2732
|
return {
|
|
2220
2733
|
tierId,
|
|
2221
2734
|
...label ? { label } : {},
|
|
@@ -2267,37 +2780,37 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2267
2780
|
const ids = /* @__PURE__ */ new Set();
|
|
2268
2781
|
const slugs = /* @__PURE__ */ new Set();
|
|
2269
2782
|
const pages = [];
|
|
2270
|
-
for (const [
|
|
2271
|
-
const path = `document.pages[${
|
|
2272
|
-
const
|
|
2273
|
-
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) {
|
|
2274
2787
|
errors.push(`${path} must be an object`);
|
|
2275
2788
|
continue;
|
|
2276
2789
|
}
|
|
2277
|
-
unknownKeys(
|
|
2278
|
-
const
|
|
2279
|
-
const slug = textValue(
|
|
2280
|
-
const title = textValue(
|
|
2281
|
-
if (
|
|
2282
|
-
if (slug && !
|
|
2283
|
-
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}"`);
|
|
2284
2797
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2285
|
-
ids.add(
|
|
2798
|
+
ids.add(id6);
|
|
2286
2799
|
slugs.add(slug);
|
|
2287
|
-
const navRaw = record(
|
|
2288
|
-
let navigation = { section: "General", order:
|
|
2800
|
+
const navRaw = record(raw2.navigation);
|
|
2801
|
+
let navigation = { section: "General", order: index2 };
|
|
2289
2802
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2290
2803
|
else {
|
|
2291
2804
|
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2292
2805
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2293
|
-
const
|
|
2294
|
-
if (!Number.isInteger(
|
|
2806
|
+
const order2 = navRaw.order;
|
|
2807
|
+
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
2295
2808
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2296
2809
|
}
|
|
2297
2810
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2298
|
-
navigation = { section, order: Number.isInteger(
|
|
2811
|
+
navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
|
|
2299
2812
|
}
|
|
2300
|
-
const audienceRaw = record(
|
|
2813
|
+
const audienceRaw = record(raw2.audience);
|
|
2301
2814
|
let audience = { kind: "all-members" };
|
|
2302
2815
|
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2303
2816
|
else {
|
|
@@ -2319,12 +2832,12 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2319
2832
|
audience = { kind: "tiers", tierIds: normalized };
|
|
2320
2833
|
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2321
2834
|
}
|
|
2322
|
-
const summary = textValue(
|
|
2323
|
-
const teaserMarkdown = textValue(
|
|
2324
|
-
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) ?? "";
|
|
2325
2838
|
let upsell;
|
|
2326
|
-
if (
|
|
2327
|
-
const upsellRaw = record(
|
|
2839
|
+
if (raw2.upsell !== void 0) {
|
|
2840
|
+
const upsellRaw = record(raw2.upsell);
|
|
2328
2841
|
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2329
2842
|
else {
|
|
2330
2843
|
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
@@ -2338,13 +2851,13 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2338
2851
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2339
2852
|
}
|
|
2340
2853
|
}
|
|
2341
|
-
pages.push({ id:
|
|
2854
|
+
pages.push({ id: id6, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2342
2855
|
}
|
|
2343
2856
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2344
|
-
for (const [
|
|
2857
|
+
for (const [index2, page] of pages.entries()) {
|
|
2345
2858
|
const parentId = page.navigation.parentId;
|
|
2346
|
-
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${
|
|
2347
|
-
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`);
|
|
2348
2861
|
}
|
|
2349
2862
|
for (const page of pages) {
|
|
2350
2863
|
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
@@ -2365,21 +2878,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2365
2878
|
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2366
2879
|
const seen = /* @__PURE__ */ new Set();
|
|
2367
2880
|
upsellOverrides = [];
|
|
2368
|
-
for (const [
|
|
2369
|
-
const path = `document.upsellOverrides[${
|
|
2370
|
-
const
|
|
2371
|
-
if (!
|
|
2881
|
+
for (const [index2, value] of values.entries()) {
|
|
2882
|
+
const path = `document.upsellOverrides[${index2}]`;
|
|
2883
|
+
const raw2 = record(value);
|
|
2884
|
+
if (!raw2) {
|
|
2372
2885
|
errors.push(`${path} must be an object`);
|
|
2373
2886
|
continue;
|
|
2374
2887
|
}
|
|
2375
|
-
unknownKeys(
|
|
2376
|
-
const pageId = textValue(
|
|
2888
|
+
unknownKeys(raw2, OVERRIDE_KEYS, path, errors);
|
|
2889
|
+
const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2377
2890
|
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2378
2891
|
seen.add(pageId);
|
|
2379
2892
|
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2380
2893
|
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2381
2894
|
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2382
|
-
const offer = offerValue(
|
|
2895
|
+
const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2383
2896
|
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2384
2897
|
}
|
|
2385
2898
|
}
|
|
@@ -2504,7 +3017,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
2504
3017
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
2505
3018
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2506
3019
|
}
|
|
2507
|
-
function
|
|
3020
|
+
function text2(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
2508
3021
|
if (value === void 0 && !required) return void 0;
|
|
2509
3022
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
2510
3023
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -2514,23 +3027,23 @@ function text(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
|
2514
3027
|
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2515
3028
|
return result;
|
|
2516
3029
|
}
|
|
2517
|
-
function
|
|
3030
|
+
function integer2(value, path, errors, minimum = 1) {
|
|
2518
3031
|
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
2519
3032
|
return Number(value);
|
|
2520
3033
|
}
|
|
2521
3034
|
function scopeValue(value, path, errors) {
|
|
2522
|
-
const
|
|
2523
|
-
if (!
|
|
3035
|
+
const raw2 = record2(value);
|
|
3036
|
+
if (!raw2) {
|
|
2524
3037
|
errors.push(`${path} must be an object`);
|
|
2525
3038
|
return null;
|
|
2526
3039
|
}
|
|
2527
|
-
unknownKeys2(
|
|
2528
|
-
if (
|
|
2529
|
-
if (
|
|
2530
|
-
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 };
|
|
2531
3044
|
}
|
|
2532
|
-
if (
|
|
2533
|
-
const chapterId =
|
|
3045
|
+
if (raw2.kind === "chapter") {
|
|
3046
|
+
const chapterId = text2(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
2534
3047
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
2535
3048
|
return { kind: "chapter", chapterId };
|
|
2536
3049
|
}
|
|
@@ -2538,28 +3051,28 @@ function scopeValue(value, path, errors) {
|
|
|
2538
3051
|
return null;
|
|
2539
3052
|
}
|
|
2540
3053
|
function resourceValue(value, path, errors) {
|
|
2541
|
-
const
|
|
2542
|
-
if (!
|
|
3054
|
+
const raw2 = record2(value);
|
|
3055
|
+
if (!raw2) {
|
|
2543
3056
|
errors.push(`${path} must be an object`);
|
|
2544
3057
|
return null;
|
|
2545
3058
|
}
|
|
2546
|
-
unknownKeys2(
|
|
2547
|
-
const
|
|
2548
|
-
if (
|
|
2549
|
-
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) ?? "";
|
|
2550
3063
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
2551
|
-
const title =
|
|
2552
|
-
const summary =
|
|
2553
|
-
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) ?? "";
|
|
2554
3067
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
2555
|
-
const revision =
|
|
2556
|
-
const scope = scopeValue(
|
|
2557
|
-
const tags = Array.isArray(
|
|
2558
|
-
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`);
|
|
2559
3072
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
2560
3073
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
2561
|
-
const bodyMarkdown =
|
|
2562
|
-
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);
|
|
2563
3076
|
if (url) {
|
|
2564
3077
|
try {
|
|
2565
3078
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -2567,13 +3080,13 @@ function resourceValue(value, path, errors) {
|
|
|
2567
3080
|
errors.push(`${path}.url must be a valid https URL`);
|
|
2568
3081
|
}
|
|
2569
3082
|
}
|
|
2570
|
-
const asset =
|
|
2571
|
-
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`);
|
|
2572
3085
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
2573
3086
|
const brandAsset = asset ? {
|
|
2574
|
-
bookId:
|
|
2575
|
-
assetId:
|
|
2576
|
-
...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) ?? "" }
|
|
2577
3090
|
} : void 0;
|
|
2578
3091
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
2579
3092
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -2581,59 +3094,59 @@ function resourceValue(value, path, errors) {
|
|
|
2581
3094
|
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
2582
3095
|
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
2583
3096
|
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
2584
|
-
const provenanceRaw = record2(
|
|
3097
|
+
const provenanceRaw = record2(raw2.provenance);
|
|
2585
3098
|
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
2586
3099
|
if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
2587
3100
|
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
|
|
2588
3101
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
2589
3102
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
2590
3103
|
const promotedFrom = promotedRaw ? {
|
|
2591
|
-
chapterId:
|
|
2592
|
-
resourceId:
|
|
2593
|
-
resourceRevision:
|
|
2594
|
-
documentRevision:
|
|
2595
|
-
promotedBy:
|
|
2596
|
-
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)
|
|
2597
3110
|
} : void 0;
|
|
2598
3111
|
const provenance = {
|
|
2599
|
-
createdBy:
|
|
2600
|
-
createdAt:
|
|
2601
|
-
updatedBy:
|
|
2602
|
-
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),
|
|
2603
3116
|
...promotedFrom ? { promotedFrom } : {}
|
|
2604
3117
|
};
|
|
2605
|
-
return { id:
|
|
3118
|
+
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
2606
3119
|
}
|
|
2607
3120
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
2608
3121
|
const errors = [];
|
|
2609
|
-
const
|
|
2610
|
-
if (!
|
|
2611
|
-
unknownKeys2(
|
|
2612
|
-
if (
|
|
2613
|
-
const revision =
|
|
2614
|
-
const generatedAt =
|
|
2615
|
-
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);
|
|
2616
3129
|
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2617
3130
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
2618
3131
|
const sourceType = sourceRaw?.type;
|
|
2619
3132
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
2620
|
-
const sourceAppId =
|
|
3133
|
+
const sourceAppId = text2(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
2621
3134
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2622
3135
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2623
3136
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2624
|
-
const targetRaw =
|
|
2625
|
-
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");
|
|
2626
3139
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
2627
3140
|
const target = targetRaw ? {
|
|
2628
|
-
appId:
|
|
2629
|
-
environment:
|
|
3141
|
+
appId: text2(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
3142
|
+
environment: text2(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
2630
3143
|
} : void 0;
|
|
2631
3144
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
2632
|
-
if (!Array.isArray(
|
|
2633
|
-
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 : [];
|
|
2634
3147
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
2635
|
-
const resources = inputs.map((item,
|
|
2636
|
-
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);
|
|
2637
3150
|
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
2638
3151
|
for (const resource of resources) {
|
|
2639
3152
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -2750,10 +3263,10 @@ function planDelivery(input) {
|
|
|
2750
3263
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
2751
3264
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
2752
3265
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
2753
|
-
const
|
|
3266
|
+
const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
2754
3267
|
|
|
2755
3268
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
2756
|
-
return { deliver: true, transport, to, subject, text:
|
|
3269
|
+
return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
|
|
2757
3270
|
}
|
|
2758
3271
|
|
|
2759
3272
|
// src/notify.ts
|
|
@@ -2788,9 +3301,9 @@ async function sendTemplated(deps, input) {
|
|
|
2788
3301
|
error = e instanceof Error ? e.message : String(e);
|
|
2789
3302
|
}
|
|
2790
3303
|
}
|
|
2791
|
-
const
|
|
3304
|
+
const id6 = deps.newId();
|
|
2792
3305
|
const row = {
|
|
2793
|
-
id:
|
|
3306
|
+
id: id6,
|
|
2794
3307
|
groupId: input.group.id,
|
|
2795
3308
|
to: decision.to,
|
|
2796
3309
|
template: input.template,
|
|
@@ -2804,7 +3317,7 @@ async function sendTemplated(deps, input) {
|
|
|
2804
3317
|
...messageId ? { messageId } : {},
|
|
2805
3318
|
...error ? { error } : {}
|
|
2806
3319
|
};
|
|
2807
|
-
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}` });
|
|
2808
3321
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
2809
3322
|
}
|
|
2810
3323
|
function emailGroupFrom(row) {
|
|
@@ -2990,29 +3503,29 @@ function validateAdmissionGrantDocument(input) {
|
|
|
2990
3503
|
const grants = [];
|
|
2991
3504
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2992
3505
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
2993
|
-
for (const [
|
|
2994
|
-
const grant =
|
|
2995
|
-
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 : "";
|
|
2996
3509
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
2997
3510
|
const normalized = normalizeAdmissionEmail(email);
|
|
2998
3511
|
const decision = grant.decision;
|
|
2999
3512
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
3000
3513
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
3001
|
-
if (!ID3.test(
|
|
3002
|
-
if (seenIds.has(
|
|
3003
|
-
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`);
|
|
3004
3517
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
3005
3518
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
3006
|
-
errors.push(`document.grants[${
|
|
3519
|
+
errors.push(`document.grants[${index2}].decision is invalid`);
|
|
3007
3520
|
}
|
|
3008
|
-
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`);
|
|
3009
3522
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
3010
|
-
errors.push(`document.grants[${
|
|
3523
|
+
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
3011
3524
|
}
|
|
3012
|
-
seenIds.add(
|
|
3525
|
+
seenIds.add(id6);
|
|
3013
3526
|
seenEmails.add(normalized);
|
|
3014
3527
|
grants.push({
|
|
3015
|
-
id:
|
|
3528
|
+
id: id6,
|
|
3016
3529
|
email,
|
|
3017
3530
|
decision,
|
|
3018
3531
|
...note ? { note } : {},
|
|
@@ -3083,8 +3596,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3083
3596
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
3084
3597
|
const ops = [];
|
|
3085
3598
|
for (const row of current) {
|
|
3086
|
-
const
|
|
3087
|
-
if (
|
|
3599
|
+
const id6 = String(row.id ?? "");
|
|
3600
|
+
if (id6 && !incomingIds.has(id6)) ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
|
|
3088
3601
|
active: false,
|
|
3089
3602
|
revokedAt: receivedAt,
|
|
3090
3603
|
revokedBy: document.source.actor,
|
|
@@ -3093,10 +3606,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3093
3606
|
} });
|
|
3094
3607
|
}
|
|
3095
3608
|
for (const grant of document.grants) {
|
|
3096
|
-
const
|
|
3097
|
-
const existing = current.find((row) => row.id ===
|
|
3098
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
3099
|
-
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,
|
|
3100
3613
|
sourceGrantId: grant.id,
|
|
3101
3614
|
sourceScope: "parent",
|
|
3102
3615
|
sourceId: document.source.appId,
|
|
@@ -3117,7 +3630,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3117
3630
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
3118
3631
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
3119
3632
|
];
|
|
3120
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
3633
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id6, attrs: retract });
|
|
3121
3634
|
}
|
|
3122
3635
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
3123
3636
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -3159,7 +3672,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
3159
3672
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
3160
3673
|
var CURRENCY = /^[a-z]{3}$/;
|
|
3161
3674
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3162
|
-
var
|
|
3675
|
+
var text3 = (value, path, errors, max) => {
|
|
3163
3676
|
if (typeof value !== "string" || !value.trim()) {
|
|
3164
3677
|
errors.push(`${path} is required`);
|
|
3165
3678
|
return "";
|
|
@@ -3178,32 +3691,32 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3178
3691
|
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
3179
3692
|
const ids = /* @__PURE__ */ new Set();
|
|
3180
3693
|
const offers = [];
|
|
3181
|
-
for (const [
|
|
3182
|
-
const path = `document.giftOffers.offers[${
|
|
3183
|
-
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);
|
|
3184
3697
|
if (!item) {
|
|
3185
3698
|
errors.push(`${path} must be an object`);
|
|
3186
3699
|
continue;
|
|
3187
3700
|
}
|
|
3188
3701
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
3189
3702
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3190
|
-
const
|
|
3191
|
-
if (
|
|
3192
|
-
if (ids.has(
|
|
3193
|
-
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);
|
|
3194
3707
|
const targetRaw = record3(item.target);
|
|
3195
3708
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
3196
3709
|
if (targetRaw?.type === "tier") {
|
|
3197
|
-
const tierId =
|
|
3710
|
+
const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
3198
3711
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
3199
3712
|
target = { type: "tier", tierId };
|
|
3200
3713
|
} else if (targetRaw?.type === "entitlement") {
|
|
3201
|
-
const entitlementKey =
|
|
3714
|
+
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
3202
3715
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
3203
3716
|
target = { type: "entitlement", entitlementKey };
|
|
3204
3717
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
3205
3718
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
3206
|
-
const stripePriceId =
|
|
3719
|
+
const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
3207
3720
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
3208
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`);
|
|
3209
3722
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -3216,7 +3729,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3216
3729
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
3217
3730
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
3218
3731
|
offers.push({
|
|
3219
|
-
id:
|
|
3732
|
+
id: id6,
|
|
3220
3733
|
target,
|
|
3221
3734
|
billingMode: item.billingMode,
|
|
3222
3735
|
stripePriceId,
|
|
@@ -3225,9 +3738,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3225
3738
|
redemptionLifetimeHours: lifetime,
|
|
3226
3739
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
3227
3740
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
3228
|
-
title:
|
|
3229
|
-
description:
|
|
3230
|
-
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)
|
|
3231
3744
|
});
|
|
3232
3745
|
}
|
|
3233
3746
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -3272,8 +3785,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
3272
3785
|
const typeKey = type.toLowerCase();
|
|
3273
3786
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
3274
3787
|
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
3275
|
-
const
|
|
3276
|
-
if (readable &&
|
|
3788
|
+
const raw2 = `${prefix}:${hubRecordId}`;
|
|
3789
|
+
if (readable && raw2.length <= 64) return raw2;
|
|
3277
3790
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
3278
3791
|
}
|
|
3279
3792
|
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
@@ -3543,14 +4056,14 @@ function periodSeries(points, now, count, periodMs) {
|
|
|
3543
4056
|
for (const point of points) {
|
|
3544
4057
|
if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
|
|
3545
4058
|
if (point.t >= start && point.t <= now) {
|
|
3546
|
-
const
|
|
3547
|
-
current[
|
|
4059
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
|
|
4060
|
+
current[index2] = (current[index2] ?? 0) + point.v;
|
|
3548
4061
|
} else if (point.t >= previousStart && point.t < start) {
|
|
3549
|
-
const
|
|
3550
|
-
previous[
|
|
4062
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
|
|
4063
|
+
previous[index2] = (previous[index2] ?? 0) + point.v;
|
|
3551
4064
|
}
|
|
3552
4065
|
}
|
|
3553
|
-
const labels = current.map((_,
|
|
4066
|
+
const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
|
|
3554
4067
|
return { labels, current, previous };
|
|
3555
4068
|
}
|
|
3556
4069
|
function dashboardMetricData(points, now) {
|
|
@@ -3634,9 +4147,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
3634
4147
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
3635
4148
|
if (!found.ok) return false;
|
|
3636
4149
|
const users = await found.json().catch(() => null);
|
|
3637
|
-
const
|
|
3638
|
-
if (!
|
|
3639
|
-
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);
|
|
3640
4153
|
}
|
|
3641
4154
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
3642
4155
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -3699,8 +4212,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
3699
4212
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
3700
4213
|
return user ? toRecord(user) : null;
|
|
3701
4214
|
}
|
|
3702
|
-
async function clerkGetUser(secretKey,
|
|
3703
|
-
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);
|
|
3704
4217
|
return data ? toRecord(data) : null;
|
|
3705
4218
|
}
|
|
3706
4219
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -3716,8 +4229,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
3716
4229
|
}
|
|
3717
4230
|
return out;
|
|
3718
4231
|
}
|
|
3719
|
-
async function clerkSetRole(secretKey,
|
|
3720
|
-
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`, {
|
|
3721
4234
|
method: "PATCH",
|
|
3722
4235
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
3723
4236
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -3943,8 +4456,8 @@ function chapterBrandFromTokens(compiled, base = {}) {
|
|
|
3943
4456
|
function isReconcilable(meeting, now) {
|
|
3944
4457
|
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
3945
4458
|
}
|
|
3946
|
-
function reconcileMeetings(meetings2,
|
|
3947
|
-
const byEvent = new Map(
|
|
4459
|
+
function reconcileMeetings(meetings2, events2, now) {
|
|
4460
|
+
const byEvent = new Map(events2.map((e) => [e.eventId, e]));
|
|
3948
4461
|
const decisions = [];
|
|
3949
4462
|
for (const m of meetings2) {
|
|
3950
4463
|
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
@@ -4154,10 +4667,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
4154
4667
|
errors.push(`${path} must be a string`);
|
|
4155
4668
|
return void 0;
|
|
4156
4669
|
}
|
|
4157
|
-
const
|
|
4158
|
-
if (required && !
|
|
4159
|
-
if (
|
|
4160
|
-
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;
|
|
4161
4674
|
}
|
|
4162
4675
|
function validateMembershipPageDocument(input, options = {}) {
|
|
4163
4676
|
const errors = [];
|
|
@@ -4175,9 +4688,9 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4175
4688
|
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
4176
4689
|
const seen = /* @__PURE__ */ new Set();
|
|
4177
4690
|
const tiers2 = [];
|
|
4178
|
-
for (const [
|
|
4179
|
-
const path = `document.tiers[${
|
|
4180
|
-
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);
|
|
4181
4694
|
if (!item) {
|
|
4182
4695
|
errors.push(`${path} must be an object`);
|
|
4183
4696
|
continue;
|
|
@@ -4206,8 +4719,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4206
4719
|
else {
|
|
4207
4720
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
4208
4721
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
4209
|
-
const
|
|
4210
|
-
return
|
|
4722
|
+
const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
4723
|
+
return text5 ? [text5] : [];
|
|
4211
4724
|
});
|
|
4212
4725
|
}
|
|
4213
4726
|
}
|
|
@@ -4283,7 +4796,7 @@ function membershipPageDocument(input, options = {}) {
|
|
|
4283
4796
|
}
|
|
4284
4797
|
|
|
4285
4798
|
// src/signup-control-validate.ts
|
|
4286
|
-
var
|
|
4799
|
+
var SLUG9 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4287
4800
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
4288
4801
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
4289
4802
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
@@ -4306,7 +4819,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
4306
4819
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
4307
4820
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
4308
4821
|
}
|
|
4309
|
-
function
|
|
4822
|
+
function text4(value, path, errors, max = 65536) {
|
|
4310
4823
|
if (typeof value !== "string" || !value.trim()) {
|
|
4311
4824
|
errors.push(`${path} is required`);
|
|
4312
4825
|
return "";
|
|
@@ -4321,19 +4834,19 @@ function tiersOf(value, errors) {
|
|
|
4321
4834
|
}
|
|
4322
4835
|
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
4323
4836
|
const seen = /* @__PURE__ */ new Set();
|
|
4324
|
-
return value.slice(0, 20).flatMap((
|
|
4325
|
-
const path = `document.tiers[${
|
|
4326
|
-
const item = recordOf(
|
|
4837
|
+
return value.slice(0, 20).flatMap((raw2, index2) => {
|
|
4838
|
+
const path = `document.tiers[${index2}]`;
|
|
4839
|
+
const item = recordOf(raw2);
|
|
4327
4840
|
if (!item) {
|
|
4328
4841
|
errors.push(`${path} must be an object`);
|
|
4329
4842
|
return [];
|
|
4330
4843
|
}
|
|
4331
4844
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
4332
|
-
const
|
|
4333
|
-
if (
|
|
4334
|
-
if (seen.has(
|
|
4335
|
-
seen.add(
|
|
4336
|
-
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);
|
|
4337
4850
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
4338
4851
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
4339
4852
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -4345,9 +4858,9 @@ function tiersOf(value, errors) {
|
|
|
4345
4858
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
4346
4859
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
4347
4860
|
}
|
|
4348
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
4861
|
+
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
4349
4862
|
return [{
|
|
4350
|
-
id:
|
|
4863
|
+
id: id6,
|
|
4351
4864
|
name,
|
|
4352
4865
|
available: item.available === true,
|
|
4353
4866
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -4401,7 +4914,7 @@ function policyOf(value, errors) {
|
|
|
4401
4914
|
const result = {};
|
|
4402
4915
|
for (const key of allowed2) {
|
|
4403
4916
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
4404
|
-
result[key] =
|
|
4917
|
+
result[key] = text4(input[key], `document.policy.${key}`, errors);
|
|
4405
4918
|
}
|
|
4406
4919
|
return result;
|
|
4407
4920
|
}
|
|
@@ -4439,18 +4952,18 @@ function validateSignupControlDocument(input) {
|
|
|
4439
4952
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
4440
4953
|
const source = recordOf(root.source);
|
|
4441
4954
|
const target = recordOf(root.target);
|
|
4442
|
-
const appId =
|
|
4443
|
-
const chapterId =
|
|
4444
|
-
const environment =
|
|
4445
|
-
if (appId && !
|
|
4446
|
-
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");
|
|
4447
4960
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
4448
4961
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
4449
4962
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
4450
4963
|
const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
4451
4964
|
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
4452
4965
|
if (!page.ok) errors.push(...page.errors);
|
|
4453
|
-
else if (page.value.tiers.some((tier,
|
|
4966
|
+
else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
|
|
4454
4967
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
4455
4968
|
}
|
|
4456
4969
|
const application = applicationOf(root.application, errors);
|
|
@@ -4497,14 +5010,14 @@ function validatedTokens(input) {
|
|
|
4497
5010
|
}
|
|
4498
5011
|
const tokens = {};
|
|
4499
5012
|
let serializedLength = 0;
|
|
4500
|
-
for (const [name,
|
|
5013
|
+
for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
4501
5014
|
if (!TOKEN_NAME.test(name)) {
|
|
4502
5015
|
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
4503
5016
|
}
|
|
4504
|
-
if (typeof
|
|
5017
|
+
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
4505
5018
|
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
4506
5019
|
}
|
|
4507
|
-
const value = String(
|
|
5020
|
+
const value = String(raw2).trim();
|
|
4508
5021
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
4509
5022
|
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
4510
5023
|
}
|
|
@@ -4611,16 +5124,16 @@ function markdownSection(rawHeading) {
|
|
|
4611
5124
|
}
|
|
4612
5125
|
function markdownBenefits(lines, path, errors) {
|
|
4613
5126
|
const benefits = [];
|
|
4614
|
-
for (const [
|
|
5127
|
+
for (const [index2, line] of lines.entries()) {
|
|
4615
5128
|
if (!line.trim()) continue;
|
|
4616
5129
|
const match = line.match(MARKDOWN_BENEFIT);
|
|
4617
5130
|
if (!match) {
|
|
4618
|
-
errors.push(`${path} line ${
|
|
5131
|
+
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
4619
5132
|
continue;
|
|
4620
5133
|
}
|
|
4621
|
-
const
|
|
4622
|
-
if (
|
|
4623
|
-
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);
|
|
4624
5137
|
}
|
|
4625
5138
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
4626
5139
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -4635,7 +5148,7 @@ function applyMembershipTierMarkdown(presentation, source) {
|
|
|
4635
5148
|
const sections = /* @__PURE__ */ new Map();
|
|
4636
5149
|
const prelude = [];
|
|
4637
5150
|
let current = null;
|
|
4638
|
-
for (const [
|
|
5151
|
+
for (const [index2, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
4639
5152
|
const heading = line.match(MARKDOWN_SECTION);
|
|
4640
5153
|
if (heading) {
|
|
4641
5154
|
const headingText = heading[1] ?? "";
|
|
@@ -4655,7 +5168,7 @@ function applyMembershipTierMarkdown(presentation, source) {
|
|
|
4655
5168
|
continue;
|
|
4656
5169
|
}
|
|
4657
5170
|
if (MARKDOWN_HEADING.test(line)) {
|
|
4658
|
-
errors.push(`membership Markdown line ${
|
|
5171
|
+
errors.push(`membership Markdown line ${index2 + 1} uses an unsupported heading`);
|
|
4659
5172
|
continue;
|
|
4660
5173
|
}
|
|
4661
5174
|
if (current) sections.get(current)?.lines.push(line);
|
|
@@ -4722,7 +5235,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
4722
5235
|
id: chapterId,
|
|
4723
5236
|
reason: "provider_identity",
|
|
4724
5237
|
paths: [
|
|
4725
|
-
...paidTierIds.map((
|
|
5238
|
+
...paidTierIds.map((id6) => `tiers[${id6}].stripePriceId`),
|
|
4726
5239
|
"groups.stripePublishableKey",
|
|
4727
5240
|
"applications.stripeCustomerId",
|
|
4728
5241
|
"applications.stripeSubscriptionId",
|
|
@@ -4764,9 +5277,9 @@ async function exportChapterPromotion(input) {
|
|
|
4764
5277
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
4765
5278
|
receiptDigests: [input.digest]
|
|
4766
5279
|
};
|
|
4767
|
-
const artifact = (kind,
|
|
5280
|
+
const artifact = (kind, id6, content) => ({
|
|
4768
5281
|
kind,
|
|
4769
|
-
id:
|
|
5282
|
+
id: id6,
|
|
4770
5283
|
sourceRevision: revision,
|
|
4771
5284
|
provenance: promotionProvenance,
|
|
4772
5285
|
content: canonicalPromotionValue(content)
|
|
@@ -4805,7 +5318,7 @@ async function exportChapterPromotion(input) {
|
|
|
4805
5318
|
}
|
|
4806
5319
|
|
|
4807
5320
|
// src/member-system-authority.ts
|
|
4808
|
-
var rows2 = (group) => group.fields.map((
|
|
5321
|
+
var rows2 = (group) => group.fields.map((id6) => ({ ...group, id: id6 }));
|
|
4809
5322
|
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
4810
5323
|
...rows2({
|
|
4811
5324
|
scope: "network",
|
|
@@ -5190,10 +5703,10 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
5190
5703
|
if (supplied.length === keys.length) return "apply";
|
|
5191
5704
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
5192
5705
|
}
|
|
5193
|
-
function planMemberSystemEventReplay(
|
|
5706
|
+
function planMemberSystemEventReplay(events2) {
|
|
5194
5707
|
const seen = /* @__PURE__ */ new Map();
|
|
5195
5708
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
5196
|
-
for (const event of
|
|
5709
|
+
for (const event of events2) {
|
|
5197
5710
|
const digest = seen.get(event.id);
|
|
5198
5711
|
if (digest === void 0) {
|
|
5199
5712
|
seen.set(event.id, event.digest);
|
|
@@ -5348,6 +5861,7 @@ export {
|
|
|
5348
5861
|
joinConfig,
|
|
5349
5862
|
joinConfigTiers,
|
|
5350
5863
|
leaderCrmConfig,
|
|
5864
|
+
listStripeObjects,
|
|
5351
5865
|
loadLatestAdmissionRevision,
|
|
5352
5866
|
loadOfferableTiers,
|
|
5353
5867
|
loadTiers,
|
|
@@ -5362,9 +5876,11 @@ export {
|
|
|
5362
5876
|
memberSystemRollbackPlan,
|
|
5363
5877
|
membershipPageDocument,
|
|
5364
5878
|
membershipTierPresentationFromMarkdown,
|
|
5879
|
+
networkCommercialSourceKey,
|
|
5365
5880
|
networkSourceTag,
|
|
5366
5881
|
normalizeAdmissionEmail,
|
|
5367
5882
|
normalizeChapterEmail,
|
|
5883
|
+
normalizeNetworkCommercialStripeEvent,
|
|
5368
5884
|
normalizeSharedRecord,
|
|
5369
5885
|
normalizeWebhookEvent,
|
|
5370
5886
|
offerableTiers,
|
|
@@ -5377,10 +5893,12 @@ export {
|
|
|
5377
5893
|
projectBuilderLibraryDelivery,
|
|
5378
5894
|
projectMemberContent,
|
|
5379
5895
|
projectMemberSelf,
|
|
5896
|
+
projectNetworkCommercialEvents,
|
|
5380
5897
|
projectSharedRecord,
|
|
5381
5898
|
promoteBuilderLibraryResource,
|
|
5382
5899
|
publicGiftOffers,
|
|
5383
5900
|
reconcileMeetings,
|
|
5901
|
+
reconcileStripeSnapshot,
|
|
5384
5902
|
refundedPatch,
|
|
5385
5903
|
render,
|
|
5386
5904
|
renderSignupControlThemeHead,
|
|
@@ -5394,6 +5912,8 @@ export {
|
|
|
5394
5912
|
resolveJoinContinuation,
|
|
5395
5913
|
resolveLeaderFormation,
|
|
5396
5914
|
resolveMemberContent,
|
|
5915
|
+
resolveNetworkCommercialReadModel,
|
|
5916
|
+
resolveNetworkPersonRegistry,
|
|
5397
5917
|
resolvePipeline,
|
|
5398
5918
|
resolveScheduling,
|
|
5399
5919
|
resolveSignupControl,
|