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