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