@odla-ai/chapter 0.43.2 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/{copy-context-Bh1EZvCg.d.ts → copy-context-CIxmneHI.d.ts} +42 -0
- package/dist/index.cjs +795 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -3
- package/dist/index.d.ts +155 -3
- package/dist/index.js +795 -275
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +1476 -287
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +42 -0
- package/dist/worker/index.d.ts +42 -0
- package/dist/worker/index.js +1476 -287
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -122,6 +122,7 @@ __export(index_exports, {
|
|
|
122
122
|
joinConfig: () => joinConfig,
|
|
123
123
|
joinConfigTiers: () => joinConfigTiers,
|
|
124
124
|
leaderCrmConfig: () => leaderCrmConfig,
|
|
125
|
+
listStripeObjects: () => listStripeObjects,
|
|
125
126
|
loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
|
|
126
127
|
loadOfferableTiers: () => loadOfferableTiers,
|
|
127
128
|
loadTiers: () => loadTiers,
|
|
@@ -136,9 +137,11 @@ __export(index_exports, {
|
|
|
136
137
|
memberSystemRollbackPlan: () => memberSystemRollbackPlan,
|
|
137
138
|
membershipPageDocument: () => membershipPageDocument,
|
|
138
139
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
140
|
+
networkCommercialSourceKey: () => networkCommercialSourceKey,
|
|
139
141
|
networkSourceTag: () => networkSourceTag,
|
|
140
142
|
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
141
143
|
normalizeChapterEmail: () => normalizeChapterEmail,
|
|
144
|
+
normalizeNetworkCommercialStripeEvent: () => normalizeNetworkCommercialStripeEvent,
|
|
142
145
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
143
146
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
144
147
|
offerableTiers: () => offerableTiers,
|
|
@@ -151,10 +154,12 @@ __export(index_exports, {
|
|
|
151
154
|
projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
|
|
152
155
|
projectMemberContent: () => projectMemberContent,
|
|
153
156
|
projectMemberSelf: () => projectMemberSelf,
|
|
157
|
+
projectNetworkCommercialEvents: () => projectNetworkCommercialEvents,
|
|
154
158
|
projectSharedRecord: () => projectSharedRecord,
|
|
155
159
|
promoteBuilderLibraryResource: () => promoteBuilderLibraryResource,
|
|
156
160
|
publicGiftOffers: () => publicGiftOffers,
|
|
157
161
|
reconcileMeetings: () => reconcileMeetings,
|
|
162
|
+
reconcileStripeSnapshot: () => reconcileStripeSnapshot,
|
|
158
163
|
refundedPatch: () => refundedPatch,
|
|
159
164
|
render: () => render,
|
|
160
165
|
renderSignupControlThemeHead: () => renderSignupControlThemeHead,
|
|
@@ -168,6 +173,8 @@ __export(index_exports, {
|
|
|
168
173
|
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
169
174
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
170
175
|
resolveMemberContent: () => resolveMemberContent,
|
|
176
|
+
resolveNetworkCommercialReadModel: () => resolveNetworkCommercialReadModel,
|
|
177
|
+
resolveNetworkPersonRegistry: () => resolveNetworkPersonRegistry,
|
|
171
178
|
resolvePipeline: () => resolvePipeline,
|
|
172
179
|
resolveScheduling: () => resolveScheduling,
|
|
173
180
|
resolveSignupControl: () => resolveSignupControl,
|
|
@@ -240,7 +247,7 @@ function resolveApplication(a) {
|
|
|
240
247
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
|
-
const duplicate = [...required, ...optional].find((field,
|
|
250
|
+
const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
244
251
|
if (duplicate) {
|
|
245
252
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
246
253
|
}
|
|
@@ -319,8 +326,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
319
326
|
if (app.requireDisclaimerAck && !acked) {
|
|
320
327
|
return { ok: false, error: "disclaimerAck is required" };
|
|
321
328
|
}
|
|
322
|
-
const
|
|
323
|
-
const row = { id:
|
|
329
|
+
const id6 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
330
|
+
const row = { id: id6, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
324
331
|
for (const f of [...app.required, ...app.optional]) {
|
|
325
332
|
if (fieldStates[f]?.visible === false) continue;
|
|
326
333
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
@@ -344,19 +351,19 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
344
351
|
}
|
|
345
352
|
if (acked) row.disclaimerAckAt = opts.now;
|
|
346
353
|
const ops = [
|
|
347
|
-
{ t: "update", ns: "applications", id:
|
|
354
|
+
{ t: "update", ns: "applications", id: id6, attrs: row }
|
|
348
355
|
];
|
|
349
356
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
350
357
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
351
358
|
consumedAt: opts.now,
|
|
352
|
-
consumedApplicationId:
|
|
359
|
+
consumedApplicationId: id6
|
|
353
360
|
} });
|
|
354
361
|
}
|
|
355
362
|
const { duplicate } = await db.transact(
|
|
356
363
|
ops,
|
|
357
364
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
358
365
|
);
|
|
359
|
-
return { ok: true, id:
|
|
366
|
+
return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
360
367
|
}
|
|
361
368
|
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
362
369
|
return {
|
|
@@ -670,6 +677,146 @@ var stripeEventReceiptsEntity = {
|
|
|
670
677
|
}
|
|
671
678
|
};
|
|
672
679
|
|
|
680
|
+
// src/schema-network-person.ts
|
|
681
|
+
var string = (flags = {}) => ({
|
|
682
|
+
type: "string",
|
|
683
|
+
unique: flags.unique ?? false,
|
|
684
|
+
indexed: flags.indexed ?? false,
|
|
685
|
+
optional: flags.optional ?? false
|
|
686
|
+
});
|
|
687
|
+
var number = (flags = {}) => ({
|
|
688
|
+
type: "number",
|
|
689
|
+
unique: false,
|
|
690
|
+
indexed: flags.indexed ?? false,
|
|
691
|
+
optional: flags.optional ?? false
|
|
692
|
+
});
|
|
693
|
+
var json = (flags = {}) => ({
|
|
694
|
+
type: "json",
|
|
695
|
+
unique: false,
|
|
696
|
+
indexed: false,
|
|
697
|
+
optional: flags.optional ?? false
|
|
698
|
+
});
|
|
699
|
+
var id3 = () => string({ unique: true, indexed: true });
|
|
700
|
+
var networkPersonsEntity = { attrs: {
|
|
701
|
+
id: id3(),
|
|
702
|
+
displayName: string({ indexed: true }),
|
|
703
|
+
state: string({ indexed: true }),
|
|
704
|
+
mergedIntoId: string({ indexed: true, optional: true }),
|
|
705
|
+
contactPolicy: json(),
|
|
706
|
+
revision: number({ indexed: true }),
|
|
707
|
+
createdAt: number({ indexed: true }),
|
|
708
|
+
updatedAt: number({ indexed: true })
|
|
709
|
+
} };
|
|
710
|
+
var networkIdentityClaimsEntity = { attrs: {
|
|
711
|
+
id: id3(),
|
|
712
|
+
personId: string({ indexed: true }),
|
|
713
|
+
kind: string({ indexed: true }),
|
|
714
|
+
value: string(),
|
|
715
|
+
hint: string({ optional: true }),
|
|
716
|
+
verifiedAt: number({ indexed: true }),
|
|
717
|
+
createdAt: number({ indexed: true })
|
|
718
|
+
} };
|
|
719
|
+
var networkPersonAliasesEntity = { attrs: {
|
|
720
|
+
id: id3(),
|
|
721
|
+
sourceSystem: string({ indexed: true }),
|
|
722
|
+
sourceRecordId: string({ indexed: true }),
|
|
723
|
+
sourceKey: string({ unique: true, indexed: true }),
|
|
724
|
+
displayName: string({ indexed: true }),
|
|
725
|
+
provenance: string(),
|
|
726
|
+
consent: json(),
|
|
727
|
+
status: string({ indexed: true }),
|
|
728
|
+
personId: string({ indexed: true, optional: true }),
|
|
729
|
+
candidatePersonIds: json({ optional: true }),
|
|
730
|
+
identityClaims: json(),
|
|
731
|
+
chapterLinks: json(),
|
|
732
|
+
revision: number({ indexed: true }),
|
|
733
|
+
createdAt: number({ indexed: true }),
|
|
734
|
+
updatedAt: number({ indexed: true })
|
|
735
|
+
} };
|
|
736
|
+
var networkChapterPersonLinksEntity = { attrs: {
|
|
737
|
+
id: id3(),
|
|
738
|
+
personId: string({ indexed: true }),
|
|
739
|
+
chapterId: string({ indexed: true }),
|
|
740
|
+
chapterPersonId: string({ indexed: true }),
|
|
741
|
+
state: string({ indexed: true }),
|
|
742
|
+
createdAt: number({ indexed: true })
|
|
743
|
+
} };
|
|
744
|
+
var networkPersonEventsEntity = { attrs: {
|
|
745
|
+
id: id3(),
|
|
746
|
+
kind: string({ indexed: true }),
|
|
747
|
+
personId: string({ indexed: true, optional: true }),
|
|
748
|
+
aliasId: string({ indexed: true, optional: true }),
|
|
749
|
+
actorId: string({ indexed: true }),
|
|
750
|
+
mutationId: string({ unique: true, indexed: true }),
|
|
751
|
+
detail: json(),
|
|
752
|
+
createdAt: number({ indexed: true })
|
|
753
|
+
} };
|
|
754
|
+
var networkPersonEntities = {
|
|
755
|
+
networkPersons: networkPersonsEntity,
|
|
756
|
+
networkIdentityClaims: networkIdentityClaimsEntity,
|
|
757
|
+
networkPersonAliases: networkPersonAliasesEntity,
|
|
758
|
+
networkChapterPersonLinks: networkChapterPersonLinksEntity,
|
|
759
|
+
networkPersonEvents: networkPersonEventsEntity
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/schema-network-commercial.ts
|
|
763
|
+
var string2 = (flags = {}) => ({
|
|
764
|
+
type: "string",
|
|
765
|
+
unique: flags.unique ?? false,
|
|
766
|
+
indexed: flags.indexed ?? false,
|
|
767
|
+
optional: flags.optional ?? false
|
|
768
|
+
});
|
|
769
|
+
var number2 = (flags = {}) => ({
|
|
770
|
+
type: "number",
|
|
771
|
+
unique: false,
|
|
772
|
+
indexed: flags.indexed ?? false,
|
|
773
|
+
optional: flags.optional ?? false
|
|
774
|
+
});
|
|
775
|
+
var boolean = (flags = {}) => ({
|
|
776
|
+
type: "boolean",
|
|
777
|
+
unique: false,
|
|
778
|
+
indexed: flags.indexed ?? false,
|
|
779
|
+
optional: false
|
|
780
|
+
});
|
|
781
|
+
var json2 = () => ({ type: "json", unique: false, indexed: false, optional: false });
|
|
782
|
+
var id4 = () => string2({ unique: true, indexed: true });
|
|
783
|
+
var events = { attrs: {
|
|
784
|
+
id: id4(),
|
|
785
|
+
eventId: string2({ unique: true, indexed: true }),
|
|
786
|
+
digest: string2({ indexed: true }),
|
|
787
|
+
eventType: string2({ indexed: true }),
|
|
788
|
+
eventCreated: number2({ indexed: true }),
|
|
789
|
+
livemode: boolean({ indexed: true }),
|
|
790
|
+
fact: json2(),
|
|
791
|
+
receivedAt: number2({ indexed: true })
|
|
792
|
+
} };
|
|
793
|
+
var projection = () => ({ attrs: {
|
|
794
|
+
id: id4(),
|
|
795
|
+
providerId: string2({ indexed: true }),
|
|
796
|
+
sourceKey: string2({ indexed: true }),
|
|
797
|
+
status: string2({ indexed: true }),
|
|
798
|
+
snapshot: json2(),
|
|
799
|
+
eventCreated: number2({ indexed: true }),
|
|
800
|
+
updatedAt: number2({ indexed: true })
|
|
801
|
+
} });
|
|
802
|
+
var exceptions = { attrs: {
|
|
803
|
+
id: id4(),
|
|
804
|
+
eventId: string2({ indexed: true }),
|
|
805
|
+
kind: string2({ indexed: true }),
|
|
806
|
+
sourceKey: string2({ indexed: true, optional: true }),
|
|
807
|
+
status: string2({ indexed: true }),
|
|
808
|
+
detail: json2(),
|
|
809
|
+
updatedAt: number2({ indexed: true })
|
|
810
|
+
} };
|
|
811
|
+
var networkCommercialEntities = {
|
|
812
|
+
networkCommercialEvents: events,
|
|
813
|
+
networkCommercialCustomers: projection(),
|
|
814
|
+
networkCommercialSubscriptions: projection(),
|
|
815
|
+
networkCommercialInvoices: projection(),
|
|
816
|
+
networkCommercialRefunds: projection(),
|
|
817
|
+
networkCommercialExceptions: exceptions
|
|
818
|
+
};
|
|
819
|
+
|
|
673
820
|
// src/schema.ts
|
|
674
821
|
function attr5(type, flags = {}) {
|
|
675
822
|
return {
|
|
@@ -679,10 +826,10 @@ function attr5(type, flags = {}) {
|
|
|
679
826
|
optional: flags.optional ?? false
|
|
680
827
|
};
|
|
681
828
|
}
|
|
682
|
-
var
|
|
829
|
+
var id5 = () => attr5("string", { unique: true, indexed: true });
|
|
683
830
|
var admins = {
|
|
684
831
|
attrs: {
|
|
685
|
-
id:
|
|
832
|
+
id: id5(),
|
|
686
833
|
email: attr5("string", { unique: true, indexed: true }),
|
|
687
834
|
name: attr5("string", { optional: true }),
|
|
688
835
|
note: attr5("string", { optional: true })
|
|
@@ -690,7 +837,7 @@ var admins = {
|
|
|
690
837
|
};
|
|
691
838
|
var superAdmins = {
|
|
692
839
|
attrs: {
|
|
693
|
-
id:
|
|
840
|
+
id: id5(),
|
|
694
841
|
email: attr5("string", { unique: true, indexed: true }),
|
|
695
842
|
note: attr5("string", { optional: true }),
|
|
696
843
|
createdAt: attr5("number", { indexed: true })
|
|
@@ -698,7 +845,7 @@ var superAdmins = {
|
|
|
698
845
|
};
|
|
699
846
|
var networkNotes = {
|
|
700
847
|
attrs: {
|
|
701
|
-
id:
|
|
848
|
+
id: id5(),
|
|
702
849
|
recordKey: attr5("string", { indexed: true }),
|
|
703
850
|
targetId: attr5("string", { indexed: true }),
|
|
704
851
|
recordType: attr5("string", { indexed: true }),
|
|
@@ -711,7 +858,7 @@ var networkNotes = {
|
|
|
711
858
|
};
|
|
712
859
|
var signupControls = {
|
|
713
860
|
attrs: {
|
|
714
|
-
id:
|
|
861
|
+
id: id5(),
|
|
715
862
|
sourceId: attr5("string", { indexed: true }),
|
|
716
863
|
targetId: attr5("string", { indexed: true }),
|
|
717
864
|
environment: attr5("string", { indexed: true }),
|
|
@@ -724,7 +871,7 @@ var signupControls = {
|
|
|
724
871
|
};
|
|
725
872
|
var memberContentControls = {
|
|
726
873
|
attrs: {
|
|
727
|
-
id:
|
|
874
|
+
id: id5(),
|
|
728
875
|
sourceId: attr5("string", { indexed: true }),
|
|
729
876
|
targetId: attr5("string", { indexed: true }),
|
|
730
877
|
environment: attr5("string", { indexed: true }),
|
|
@@ -737,7 +884,7 @@ var memberContentControls = {
|
|
|
737
884
|
};
|
|
738
885
|
var memberContentLocal = {
|
|
739
886
|
attrs: {
|
|
740
|
-
id:
|
|
887
|
+
id: id5(),
|
|
741
888
|
chapterId: attr5("string", { indexed: true }),
|
|
742
889
|
environment: attr5("string", { indexed: true }),
|
|
743
890
|
revision: attr5("number", { indexed: true }),
|
|
@@ -748,7 +895,7 @@ var memberContentLocal = {
|
|
|
748
895
|
};
|
|
749
896
|
var builderLibraryControls = {
|
|
750
897
|
attrs: {
|
|
751
|
-
id:
|
|
898
|
+
id: id5(),
|
|
752
899
|
sourceId: attr5("string", { indexed: true }),
|
|
753
900
|
targetId: attr5("string", { indexed: true }),
|
|
754
901
|
environment: attr5("string", { indexed: true }),
|
|
@@ -761,7 +908,7 @@ var builderLibraryControls = {
|
|
|
761
908
|
};
|
|
762
909
|
var builderLibraryLocal = {
|
|
763
910
|
attrs: {
|
|
764
|
-
id:
|
|
911
|
+
id: id5(),
|
|
765
912
|
appId: attr5("string", { indexed: true }),
|
|
766
913
|
environment: attr5("string", { indexed: true }),
|
|
767
914
|
revision: attr5("number", { indexed: true }),
|
|
@@ -772,7 +919,7 @@ var builderLibraryLocal = {
|
|
|
772
919
|
};
|
|
773
920
|
var groups = {
|
|
774
921
|
attrs: {
|
|
775
|
-
id:
|
|
922
|
+
id: id5(),
|
|
776
923
|
name: attr5("string"),
|
|
777
924
|
standardPriceCents: attr5("number"),
|
|
778
925
|
foundingDiscountCents: attr5("number"),
|
|
@@ -795,7 +942,7 @@ var groups = {
|
|
|
795
942
|
};
|
|
796
943
|
var tiers = {
|
|
797
944
|
attrs: {
|
|
798
|
-
id:
|
|
945
|
+
id: id5(),
|
|
799
946
|
groupId: attr5("string", { indexed: true }),
|
|
800
947
|
name: attr5("string"),
|
|
801
948
|
priceCents: attr5("number"),
|
|
@@ -808,7 +955,7 @@ var tiers = {
|
|
|
808
955
|
};
|
|
809
956
|
var meetings = {
|
|
810
957
|
attrs: {
|
|
811
|
-
id:
|
|
958
|
+
id: id5(),
|
|
812
959
|
applicationId: attr5("string", { indexed: true }),
|
|
813
960
|
groupId: attr5("string", { indexed: true }),
|
|
814
961
|
startAt: attr5("number", { indexed: true }),
|
|
@@ -827,7 +974,7 @@ var meetings = {
|
|
|
827
974
|
};
|
|
828
975
|
var emailLog = {
|
|
829
976
|
attrs: {
|
|
830
|
-
id:
|
|
977
|
+
id: id5(),
|
|
831
978
|
groupId: attr5("string", { indexed: true }),
|
|
832
979
|
applicationId: attr5("string", { indexed: true, optional: true }),
|
|
833
980
|
to: attr5("string", { indexed: true }),
|
|
@@ -842,7 +989,7 @@ var emailLog = {
|
|
|
842
989
|
sentAt: attr5("number", { indexed: true })
|
|
843
990
|
}
|
|
844
991
|
};
|
|
845
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false) {
|
|
992
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false, includeNetworkPersonRegistry = false, includeNetworkCommercialReadModel = false) {
|
|
846
993
|
const entities = {};
|
|
847
994
|
if (mode === "chapter") {
|
|
848
995
|
entities.applications = applicationsFor(application);
|
|
@@ -876,6 +1023,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
876
1023
|
entities.leaderAdmissionDeliveries = leaderAdmissionDeliveriesEntity;
|
|
877
1024
|
entities.leaderAdmissionMutations = leaderAdmissionMutationsEntity;
|
|
878
1025
|
}
|
|
1026
|
+
if (includeNetworkPersonRegistry) {
|
|
1027
|
+
Object.assign(entities, networkPersonEntities);
|
|
1028
|
+
}
|
|
1029
|
+
if (includeNetworkCommercialReadModel) Object.assign(entities, networkCommercialEntities);
|
|
879
1030
|
const schema = { entities, links: {} };
|
|
880
1031
|
const rules = {};
|
|
881
1032
|
for (const ns of Object.keys(entities)) {
|
|
@@ -1047,8 +1198,8 @@ function resolveAuth(mode, auth) {
|
|
|
1047
1198
|
return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
|
|
1048
1199
|
}
|
|
1049
1200
|
function roleFromClaim(payload, auth) {
|
|
1050
|
-
const
|
|
1051
|
-
return typeof
|
|
1201
|
+
const raw2 = payload[auth.claim];
|
|
1202
|
+
return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
|
|
1052
1203
|
}
|
|
1053
1204
|
function isAdminRole(role, auth) {
|
|
1054
1205
|
return role === auth.adminRole;
|
|
@@ -1527,9 +1678,9 @@ var APPLICATION_LABELS = {
|
|
|
1527
1678
|
phone: "Phone",
|
|
1528
1679
|
state: "State"
|
|
1529
1680
|
};
|
|
1530
|
-
function humanize(
|
|
1531
|
-
const
|
|
1532
|
-
return
|
|
1681
|
+
function humanize(id6) {
|
|
1682
|
+
const text5 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1683
|
+
return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id6;
|
|
1533
1684
|
}
|
|
1534
1685
|
function applicationDefs(chapter) {
|
|
1535
1686
|
try {
|
|
@@ -1538,23 +1689,23 @@ function applicationDefs(chapter) {
|
|
|
1538
1689
|
return {};
|
|
1539
1690
|
}
|
|
1540
1691
|
}
|
|
1541
|
-
function applicationType(
|
|
1692
|
+
function applicationType(id6, def) {
|
|
1542
1693
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1543
|
-
return
|
|
1694
|
+
return id6 === "email" ? "email" : id6 === "linkedin" ? "url" : "string";
|
|
1544
1695
|
}
|
|
1545
1696
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1546
1697
|
const defs = applicationDefs(chapter);
|
|
1547
|
-
return [...application.required, ...application.optional].map((
|
|
1548
|
-
const def = defs[
|
|
1549
|
-
const conditions = application.conditions[
|
|
1550
|
-
const type = applicationType(
|
|
1698
|
+
return [...application.required, ...application.optional].map((id6, order2) => {
|
|
1699
|
+
const def = defs[id6];
|
|
1700
|
+
const conditions = application.conditions[id6];
|
|
1701
|
+
const type = applicationType(id6, def);
|
|
1551
1702
|
return {
|
|
1552
|
-
id:
|
|
1553
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
1703
|
+
id: id6,
|
|
1704
|
+
label: def?.label ?? APPLICATION_LABELS[id6] ?? humanize(id6),
|
|
1554
1705
|
type,
|
|
1555
|
-
required: application.required.includes(
|
|
1556
|
-
order,
|
|
1557
|
-
maxLength: application.maxLen[
|
|
1706
|
+
required: application.required.includes(id6),
|
|
1707
|
+
order: order2,
|
|
1708
|
+
maxLength: application.maxLen[id6] ?? application.defaultMaxLen,
|
|
1558
1709
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
1559
1710
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
1560
1711
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -1564,17 +1715,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
1564
1715
|
function resolveFormationFlowFields(crm, formation) {
|
|
1565
1716
|
if (!formation.enabled) return [];
|
|
1566
1717
|
const def = crm.type(formation.type);
|
|
1567
|
-
return [...formation.required, ...formation.optional].map((
|
|
1568
|
-
const field = def.fields[
|
|
1718
|
+
return [...formation.required, ...formation.optional].map((id6, order2) => {
|
|
1719
|
+
const field = def.fields[id6];
|
|
1569
1720
|
return {
|
|
1570
|
-
id:
|
|
1571
|
-
label: field.label ?? humanize(
|
|
1721
|
+
id: id6,
|
|
1722
|
+
label: field.label ?? humanize(id6),
|
|
1572
1723
|
type: field.type,
|
|
1573
|
-
required: formation.required.includes(
|
|
1574
|
-
order,
|
|
1575
|
-
maxLength: formation.maxLen[
|
|
1724
|
+
required: formation.required.includes(id6),
|
|
1725
|
+
order: order2,
|
|
1726
|
+
maxLength: formation.maxLen[id6] ?? formation.defaultMaxLen,
|
|
1576
1727
|
...field.options ? { options: [...field.options] } : {},
|
|
1577
|
-
...
|
|
1728
|
+
...id6 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1578
1729
|
};
|
|
1579
1730
|
});
|
|
1580
1731
|
}
|
|
@@ -1614,7 +1765,7 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1614
1765
|
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
1615
1766
|
}
|
|
1616
1767
|
}
|
|
1617
|
-
const duplicates = [...required, ...optional].filter((field,
|
|
1768
|
+
const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
1618
1769
|
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
1619
1770
|
const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
|
|
1620
1771
|
if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
|
|
@@ -1665,7 +1816,7 @@ var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
|
1665
1816
|
var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
|
|
1666
1817
|
var BINDING = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
1667
1818
|
function validateSharedNotes(opts) {
|
|
1668
|
-
const duplicate = opts.values.some((type,
|
|
1819
|
+
const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
|
|
1669
1820
|
const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type] || (opts.fields ? !opts.fields[type] : false));
|
|
1670
1821
|
if (duplicate || invalid) {
|
|
1671
1822
|
throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
|
|
@@ -1675,7 +1826,7 @@ function validateEditableFields(opts) {
|
|
|
1675
1826
|
for (const [type, names] of Object.entries(opts.values)) {
|
|
1676
1827
|
const readable = opts.readable?.[type];
|
|
1677
1828
|
const def = opts.crm.config.types[type];
|
|
1678
|
-
const duplicate = names.some((name,
|
|
1829
|
+
const duplicate = names.some((name, index2) => names.indexOf(name) !== index2);
|
|
1679
1830
|
const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
|
|
1680
1831
|
const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
|
|
1681
1832
|
if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
|
|
@@ -1686,7 +1837,7 @@ function validateEditableFields(opts) {
|
|
|
1686
1837
|
}
|
|
1687
1838
|
}
|
|
1688
1839
|
function validateStageTransitions(opts) {
|
|
1689
|
-
const duplicate = opts.values.some((type,
|
|
1840
|
+
const duplicate = opts.values.some((type, index2) => opts.values.indexOf(type) !== index2);
|
|
1690
1841
|
const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
|
|
1691
1842
|
if (duplicate || invalid) {
|
|
1692
1843
|
throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
|
|
@@ -1868,12 +2019,58 @@ function resolveNetwork(config, crm) {
|
|
|
1868
2019
|
return { targets, readers };
|
|
1869
2020
|
}
|
|
1870
2021
|
|
|
2022
|
+
// src/config-network-person.ts
|
|
2023
|
+
var SLUG2 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
2024
|
+
function resolveNetworkPersonRegistry(mode, input, targetIds) {
|
|
2025
|
+
if (!input) return null;
|
|
2026
|
+
if (mode !== "hub") throw new Error("defineChapter.networkPersonRegistry: only hub mode may own network Person authority");
|
|
2027
|
+
if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
|
|
2028
|
+
const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
|
|
2029
|
+
if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
|
|
2030
|
+
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id6) => !SLUG2.test(id6))) {
|
|
2031
|
+
throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
|
|
2032
|
+
}
|
|
2033
|
+
const unknown = chapterIds.filter((id6) => !targetIds.includes(id6));
|
|
2034
|
+
if (unknown.length) {
|
|
2035
|
+
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id6) => `"${id6}"`).join(", ")} are not configured network targets`);
|
|
2036
|
+
}
|
|
2037
|
+
return { mode: "shadow", chapterIds };
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/config-network-commercial.ts
|
|
2041
|
+
var SLUG3 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
2042
|
+
var TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
2043
|
+
function networkCommercialSourceKey(source) {
|
|
2044
|
+
return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
|
|
2045
|
+
}
|
|
2046
|
+
function resolveNetworkCommercialReadModel(mode, input, targetIds) {
|
|
2047
|
+
if (!input) return null;
|
|
2048
|
+
if (mode !== "hub") throw new Error("defineChapter.networkCommercialReadModel: only hub mode may own the network commercial projection");
|
|
2049
|
+
if (input.mode !== "shadow") throw new Error('defineChapter.networkCommercialReadModel.mode: must be "shadow"');
|
|
2050
|
+
if (!Array.isArray(input.sources) || input.sources.length === 0) {
|
|
2051
|
+
throw new Error("defineChapter.networkCommercialReadModel.sources: at least one exact source is required");
|
|
2052
|
+
}
|
|
2053
|
+
const sources = input.sources.map((source) => ({ ...source }));
|
|
2054
|
+
for (const source of sources) {
|
|
2055
|
+
if (!SLUG3.test(source.appId) || !SLUG3.test(source.chapterId) || !TOKEN.test(source.environment) || !TOKEN.test(source.dataEnvironment) || !TOKEN.test(source.runtime) || source.stripeMode !== "test" && source.stripeMode !== "live") {
|
|
2056
|
+
throw new Error("defineChapter.networkCommercialReadModel.sources: every source requires valid app, chapter, environment, data-environment, runtime, and Stripe mode values");
|
|
2057
|
+
}
|
|
2058
|
+
if (!targetIds.includes(source.chapterId)) {
|
|
2059
|
+
throw new Error(`defineChapter.networkCommercialReadModel.sources: chapter "${source.chapterId}" is not a configured network target`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (new Set(sources.map(networkCommercialSourceKey)).size !== sources.length) {
|
|
2063
|
+
throw new Error("defineChapter.networkCommercialReadModel.sources: exact source partitions must be distinct");
|
|
2064
|
+
}
|
|
2065
|
+
return { mode: "shadow", sources };
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1871
2068
|
// src/config-signup-control.ts
|
|
1872
|
-
var
|
|
2069
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1873
2070
|
var SECRET_NAME = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1874
2071
|
function resolveSignupControl(input) {
|
|
1875
2072
|
if (!input) return null;
|
|
1876
|
-
if (!
|
|
2073
|
+
if (!SLUG4.test(input.sourceId)) {
|
|
1877
2074
|
throw new Error("defineChapter.signupControl.sourceId: must be a lowercase app slug");
|
|
1878
2075
|
}
|
|
1879
2076
|
if (input.stripeMode !== "test" && input.stripeMode !== "live") {
|
|
@@ -1887,12 +2084,12 @@ function resolveSignupControl(input) {
|
|
|
1887
2084
|
}
|
|
1888
2085
|
|
|
1889
2086
|
// src/config-member-content.ts
|
|
1890
|
-
var
|
|
2087
|
+
var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1891
2088
|
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1892
2089
|
function resolveMemberContent(input) {
|
|
1893
2090
|
if (!input) return null;
|
|
1894
2091
|
const parent = input.parent;
|
|
1895
|
-
if (parent && !
|
|
2092
|
+
if (parent && !SLUG5.test(parent.sourceId)) {
|
|
1896
2093
|
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1897
2094
|
}
|
|
1898
2095
|
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
@@ -1907,22 +2104,22 @@ function resolveMemberContent(input) {
|
|
|
1907
2104
|
}
|
|
1908
2105
|
|
|
1909
2106
|
// src/config-builder-library.ts
|
|
1910
|
-
var
|
|
2107
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1911
2108
|
function resolveBuilderLibrary(mode, input, network = { targets: [], readers: [] }) {
|
|
1912
2109
|
if (!input) return null;
|
|
1913
2110
|
if (input.sources !== void 0 && !Array.isArray(input.sources)) {
|
|
1914
2111
|
throw new Error("defineChapter.builderLibrary.sources: must be an array");
|
|
1915
2112
|
}
|
|
1916
2113
|
const seen = /* @__PURE__ */ new Set();
|
|
1917
|
-
const sources = (input.sources ?? []).map((source,
|
|
1918
|
-
if (!source || typeof source !== "object" || !
|
|
1919
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
2114
|
+
const sources = (input.sources ?? []).map((source, index2) => {
|
|
2115
|
+
if (!source || typeof source !== "object" || !SLUG6.test(source.sourceId)) {
|
|
2116
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}].sourceId: must be a lowercase app slug`);
|
|
1920
2117
|
}
|
|
1921
2118
|
if (source.sourceId === "built-not-found" && mode === "hub") {
|
|
1922
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
2119
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}]: a hub accepts Chapter sources, not itself`);
|
|
1923
2120
|
}
|
|
1924
2121
|
if (mode === "chapter" && source.sourceId !== "built-not-found") {
|
|
1925
|
-
throw new Error(`defineChapter.builderLibrary.sources[${
|
|
2122
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index2}]: Chapters may accept only the built-not-found source`);
|
|
1926
2123
|
}
|
|
1927
2124
|
if (seen.has(source.sourceId)) {
|
|
1928
2125
|
throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
|
|
@@ -1930,14 +2127,14 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
|
|
|
1930
2127
|
seen.add(source.sourceId);
|
|
1931
2128
|
if ("secretName" in source) {
|
|
1932
2129
|
throw new Error(
|
|
1933
|
-
`defineChapter.builderLibrary.sources[${
|
|
2130
|
+
`defineChapter.builderLibrary.sources[${index2}].secretName: remove this field; Builder Library reuses the Chapter network edge`
|
|
1934
2131
|
);
|
|
1935
2132
|
}
|
|
1936
2133
|
const networkPeer = mode === "hub" ? network.targets.some((target) => target.id === source.sourceId) : network.readers.some((reader) => reader.id === source.sourceId);
|
|
1937
2134
|
if (!networkPeer) {
|
|
1938
2135
|
const side = mode === "hub" ? "targets" : "readers";
|
|
1939
2136
|
throw new Error(
|
|
1940
|
-
`defineChapter.builderLibrary.sources[${
|
|
2137
|
+
`defineChapter.builderLibrary.sources[${index2}]: "${source.sourceId}" must already be configured in network.${side}`
|
|
1941
2138
|
);
|
|
1942
2139
|
}
|
|
1943
2140
|
return { sourceId: source.sourceId };
|
|
@@ -1946,15 +2143,15 @@ function resolveBuilderLibrary(mode, input, network = { targets: [], readers: []
|
|
|
1946
2143
|
}
|
|
1947
2144
|
|
|
1948
2145
|
// src/config.ts
|
|
1949
|
-
var
|
|
2146
|
+
var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1950
2147
|
function isResolvedCrm(x) {
|
|
1951
2148
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1952
2149
|
}
|
|
1953
2150
|
function defineChapter(config) {
|
|
1954
2151
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1955
|
-
const { id:
|
|
1956
|
-
if (typeof
|
|
1957
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
2152
|
+
const { id: id6, name } = config;
|
|
2153
|
+
if (typeof id6 !== "string" || !SLUG7.test(id6)) {
|
|
2154
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id6)}`);
|
|
1958
2155
|
}
|
|
1959
2156
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1960
2157
|
const mode = config.mode ?? "chapter";
|
|
@@ -1977,6 +2174,16 @@ function defineChapter(config) {
|
|
|
1977
2174
|
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
1978
2175
|
const copy = resolveChapterCopy(config.copy);
|
|
1979
2176
|
const network = resolveNetwork(config, crm);
|
|
2177
|
+
const networkPersonRegistry = resolveNetworkPersonRegistry(
|
|
2178
|
+
mode,
|
|
2179
|
+
config.networkPersonRegistry,
|
|
2180
|
+
network.targets.map((target) => target.id)
|
|
2181
|
+
);
|
|
2182
|
+
const networkCommercialReadModel = resolveNetworkCommercialReadModel(
|
|
2183
|
+
mode,
|
|
2184
|
+
config.networkCommercialReadModel,
|
|
2185
|
+
network.targets.map((target) => target.id)
|
|
2186
|
+
);
|
|
1980
2187
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1981
2188
|
const memberContent = resolveMemberContent(config.memberContent);
|
|
1982
2189
|
const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary, network);
|
|
@@ -1995,7 +2202,9 @@ function defineChapter(config) {
|
|
|
1995
2202
|
Boolean(signupControl),
|
|
1996
2203
|
Boolean(memberContent),
|
|
1997
2204
|
Boolean(builderLibrary),
|
|
1998
|
-
network.targets.some((target) => target.admissionGrants)
|
|
2205
|
+
network.targets.some((target) => target.admissionGrants),
|
|
2206
|
+
Boolean(networkPersonRegistry),
|
|
2207
|
+
Boolean(networkCommercialReadModel)
|
|
1999
2208
|
);
|
|
2000
2209
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
2001
2210
|
const account = config.account ?? "none";
|
|
@@ -2045,7 +2254,7 @@ function defineChapter(config) {
|
|
|
2045
2254
|
};
|
|
2046
2255
|
const chapter = {
|
|
2047
2256
|
config,
|
|
2048
|
-
id:
|
|
2257
|
+
id: id6,
|
|
2049
2258
|
name,
|
|
2050
2259
|
mode,
|
|
2051
2260
|
brand,
|
|
@@ -2054,6 +2263,8 @@ function defineChapter(config) {
|
|
|
2054
2263
|
signupControl,
|
|
2055
2264
|
memberContent,
|
|
2056
2265
|
builderLibrary,
|
|
2266
|
+
networkPersonRegistry,
|
|
2267
|
+
networkCommercialReadModel,
|
|
2057
2268
|
formation,
|
|
2058
2269
|
crm,
|
|
2059
2270
|
auth,
|
|
@@ -2071,6 +2282,304 @@ function defineChapter(config) {
|
|
|
2071
2282
|
return chapter;
|
|
2072
2283
|
}
|
|
2073
2284
|
|
|
2285
|
+
// src/network-commercial.ts
|
|
2286
|
+
var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2287
|
+
var text = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
|
|
2288
|
+
var integer = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
2289
|
+
var bool = (value) => typeof value === "boolean" ? value : void 0;
|
|
2290
|
+
var providerId = (value, prefix) => {
|
|
2291
|
+
const valueText = text(value);
|
|
2292
|
+
return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
|
|
2293
|
+
};
|
|
2294
|
+
function metadataCandidates(object) {
|
|
2295
|
+
const parent = raw(object.parent);
|
|
2296
|
+
const subscriptionDetails = raw(object.subscription_details);
|
|
2297
|
+
const parentSubscription = raw(parent.subscription_details);
|
|
2298
|
+
return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
|
|
2299
|
+
}
|
|
2300
|
+
function metadataValue(candidates, key) {
|
|
2301
|
+
return candidates.map((candidate) => text(candidate[key])).find(Boolean);
|
|
2302
|
+
}
|
|
2303
|
+
function refId(value, prefix) {
|
|
2304
|
+
if (typeof value === "string") return providerId(value, prefix);
|
|
2305
|
+
return providerId(raw(value).id, prefix);
|
|
2306
|
+
}
|
|
2307
|
+
function subscriptionRef(object) {
|
|
2308
|
+
return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
|
|
2309
|
+
}
|
|
2310
|
+
function firstPrice(object) {
|
|
2311
|
+
const data = raw(object.items).data;
|
|
2312
|
+
const item = Array.isArray(data) ? raw(data[0]) : {};
|
|
2313
|
+
const priceId = refId(item.price, "price_");
|
|
2314
|
+
const quantity = integer(item.quantity);
|
|
2315
|
+
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
2316
|
+
}
|
|
2317
|
+
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
2318
|
+
const event = raw(input);
|
|
2319
|
+
const eventId = providerId(event.id, "evt_") ?? "";
|
|
2320
|
+
const eventType = text(event.type) ?? "";
|
|
2321
|
+
const object = raw(raw(event.data).object);
|
|
2322
|
+
const objectId = providerId(object.id) ?? "";
|
|
2323
|
+
const candidates = metadataCandidates(object);
|
|
2324
|
+
const identity = {};
|
|
2325
|
+
const identityKeys = {
|
|
2326
|
+
appId: "odlaAppId",
|
|
2327
|
+
chapterId: "chapterId",
|
|
2328
|
+
environment: "odlaEnvironment",
|
|
2329
|
+
dataEnvironment: "odlaDataEnvironment",
|
|
2330
|
+
runtime: "odlaRuntime"
|
|
2331
|
+
};
|
|
2332
|
+
for (const [target, source] of Object.entries(identityKeys)) {
|
|
2333
|
+
const value = metadataValue(candidates, source);
|
|
2334
|
+
if (value) identity[target] = value;
|
|
2335
|
+
}
|
|
2336
|
+
let objectKind = "ignored";
|
|
2337
|
+
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
2338
|
+
else if (eventType.startsWith("customer.")) objectKind = "customer";
|
|
2339
|
+
else if (eventType.startsWith("invoice.")) objectKind = "invoice";
|
|
2340
|
+
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
2341
|
+
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
2342
|
+
const customerId = refId(object.customer, "cus_");
|
|
2343
|
+
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
|
|
2344
|
+
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
|
|
2345
|
+
const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
|
|
2346
|
+
const paymentIntentId = refId(object.payment_intent, "pi_");
|
|
2347
|
+
const price = firstPrice(object);
|
|
2348
|
+
const amountCents = integer(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
|
|
2349
|
+
const currency = text(object.currency)?.toLowerCase();
|
|
2350
|
+
const status = text(object.status);
|
|
2351
|
+
return {
|
|
2352
|
+
eventId,
|
|
2353
|
+
digest,
|
|
2354
|
+
eventType,
|
|
2355
|
+
eventCreated: integer(event.created) ?? 0,
|
|
2356
|
+
livemode: event.livemode === true,
|
|
2357
|
+
objectKind,
|
|
2358
|
+
objectId,
|
|
2359
|
+
identity,
|
|
2360
|
+
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
2361
|
+
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
2362
|
+
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
2363
|
+
...customerId ? { customerId } : {},
|
|
2364
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
2365
|
+
...invoiceId ? { invoiceId } : {},
|
|
2366
|
+
...chargeId ? { chargeId } : {},
|
|
2367
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
2368
|
+
...price,
|
|
2369
|
+
...status ? { status } : {},
|
|
2370
|
+
...currency ? { currency } : {},
|
|
2371
|
+
...amountCents !== void 0 ? { amountCents } : {},
|
|
2372
|
+
...integer(object.amount_paid) !== void 0 ? { amountPaidCents: integer(object.amount_paid) } : {},
|
|
2373
|
+
...integer(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer(object.amount_remaining) } : {},
|
|
2374
|
+
...integer(object.current_period_start) !== void 0 ? { currentPeriodStart: integer(object.current_period_start) } : {},
|
|
2375
|
+
...integer(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer(object.current_period_end) } : {},
|
|
2376
|
+
...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
2380
|
+
function directSource(fact, sources) {
|
|
2381
|
+
const mode = fact.livemode ? "live" : "test";
|
|
2382
|
+
const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
|
|
2383
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
|
|
2384
|
+
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
2385
|
+
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
2386
|
+
return null;
|
|
2387
|
+
}
|
|
2388
|
+
var objectRefs = (fact) => [
|
|
2389
|
+
fact.customerId && `customer:${fact.customerId}`,
|
|
2390
|
+
fact.subscriptionId && `subscription:${fact.subscriptionId}`,
|
|
2391
|
+
fact.invoiceId && `invoice:${fact.invoiceId}`,
|
|
2392
|
+
fact.chargeId && `charge:${fact.chargeId}`,
|
|
2393
|
+
fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
|
|
2394
|
+
].filter((value) => Boolean(value));
|
|
2395
|
+
function ownRef(fact) {
|
|
2396
|
+
if (!fact.objectId) return null;
|
|
2397
|
+
if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
|
|
2398
|
+
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
2399
|
+
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
2400
|
+
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
2401
|
+
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
2402
|
+
}
|
|
2403
|
+
var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
|
|
2404
|
+
function projectNetworkCommercialEvents(input, sources) {
|
|
2405
|
+
const facts2 = [...input].sort(order);
|
|
2406
|
+
const exceptions2 = [];
|
|
2407
|
+
const unique = [];
|
|
2408
|
+
const digests = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const fact of facts2) {
|
|
2410
|
+
const prior = digests.get(fact.eventId);
|
|
2411
|
+
if (prior === void 0) {
|
|
2412
|
+
digests.set(fact.eventId, fact.digest);
|
|
2413
|
+
unique.push(fact);
|
|
2414
|
+
} else if (prior !== fact.digest) exceptions2.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
2415
|
+
}
|
|
2416
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
2417
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2418
|
+
for (const fact of unique) {
|
|
2419
|
+
const direct = directSource(fact, sources);
|
|
2420
|
+
if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
|
|
2421
|
+
}
|
|
2422
|
+
let changed = true;
|
|
2423
|
+
while (changed) {
|
|
2424
|
+
changed = false;
|
|
2425
|
+
for (const fact of unique) {
|
|
2426
|
+
const assigned = assignments.get(fact.eventId);
|
|
2427
|
+
if (assigned) {
|
|
2428
|
+
const key = networkCommercialSourceKey(assigned);
|
|
2429
|
+
for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
|
|
2430
|
+
const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
|
|
2431
|
+
const before = keys2.size;
|
|
2432
|
+
keys2.add(key);
|
|
2433
|
+
refs.set(ref, keys2);
|
|
2434
|
+
if (keys2.size !== before) changed = true;
|
|
2435
|
+
}
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
if (directSource(fact, sources) === "wrong") continue;
|
|
2439
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
2440
|
+
if (keys.size !== 1) continue;
|
|
2441
|
+
const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
|
|
2442
|
+
if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
|
|
2443
|
+
assignments.set(fact.eventId, candidate);
|
|
2444
|
+
changed = true;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2449
|
+
for (const fact of unique) {
|
|
2450
|
+
if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
|
|
2451
|
+
exceptions2.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
const source = assignments.get(fact.eventId);
|
|
2455
|
+
if (!source) {
|
|
2456
|
+
const direct = directSource(fact, sources);
|
|
2457
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
2458
|
+
exceptions2.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
|
|
2462
|
+
const key = `${fact.objectKind}:${fact.objectId}`;
|
|
2463
|
+
const prior = latest.get(key);
|
|
2464
|
+
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
2465
|
+
}
|
|
2466
|
+
const rows3 = [...latest.values()];
|
|
2467
|
+
const customers = rows3.filter((row) => row.objectKind === "customer");
|
|
2468
|
+
const subscriptions = rows3.filter((row) => row.objectKind === "subscription");
|
|
2469
|
+
const invoices = rows3.filter((row) => row.objectKind === "invoice");
|
|
2470
|
+
const refunds = rows3.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
2471
|
+
for (const charge of rows3.filter((row) => row.eventType === "charge.refunded")) {
|
|
2472
|
+
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
2473
|
+
exceptions2.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
const totalsMap = /* @__PURE__ */ new Map();
|
|
2477
|
+
const add = (row, field, amount) => {
|
|
2478
|
+
if (!row.currency) return;
|
|
2479
|
+
const key = `${row.sourceKey}:${row.currency}`;
|
|
2480
|
+
const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
2481
|
+
total[field] += amount;
|
|
2482
|
+
total.netCents = total.paidCents - total.refundedCents;
|
|
2483
|
+
totalsMap.set(key, total);
|
|
2484
|
+
};
|
|
2485
|
+
for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
|
|
2486
|
+
for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
|
|
2487
|
+
return { customers, subscriptions, invoices, refunds, exceptions: exceptions2, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/network-commercial-reconcile.ts
|
|
2491
|
+
var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2492
|
+
async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
2493
|
+
const objects = [];
|
|
2494
|
+
let startingAfter;
|
|
2495
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
|
|
2496
|
+
const page = await client.request("GET", path, {
|
|
2497
|
+
...params,
|
|
2498
|
+
limit: 100,
|
|
2499
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
2500
|
+
});
|
|
2501
|
+
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
2502
|
+
throw new Error("Stripe returned an invalid commercial list response");
|
|
2503
|
+
}
|
|
2504
|
+
const rows3 = page.data.map(asRaw).filter((row) => row !== null);
|
|
2505
|
+
if (rows3.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
2506
|
+
objects.push(...rows3);
|
|
2507
|
+
if (!page.has_more) return { objects, complete: true };
|
|
2508
|
+
const cursor = rows3.at(-1)?.id;
|
|
2509
|
+
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
2510
|
+
startingAfter = cursor;
|
|
2511
|
+
}
|
|
2512
|
+
return { objects, complete: false };
|
|
2513
|
+
}
|
|
2514
|
+
function facts(snapshots) {
|
|
2515
|
+
const groups2 = [
|
|
2516
|
+
["customer.updated", snapshots.customers.objects],
|
|
2517
|
+
["customer.subscription.updated", snapshots.subscriptions.objects],
|
|
2518
|
+
["invoice.updated", snapshots.invoices.objects],
|
|
2519
|
+
["refund.updated", snapshots.refunds.objects]
|
|
2520
|
+
];
|
|
2521
|
+
return groups2.flatMap(([eventType, objects]) => objects.map((object) => {
|
|
2522
|
+
const id6 = typeof object.id === "string" ? object.id : "invalid";
|
|
2523
|
+
return normalizeNetworkCommercialStripeEvent({
|
|
2524
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id6}`,
|
|
2525
|
+
type: eventType,
|
|
2526
|
+
created: typeof object.created === "number" ? object.created : 0,
|
|
2527
|
+
livemode: object.livemode === true,
|
|
2528
|
+
data: { object }
|
|
2529
|
+
}, `stripe-snapshot:${eventType}:${id6}`);
|
|
2530
|
+
}));
|
|
2531
|
+
}
|
|
2532
|
+
var selected = (row) => ({
|
|
2533
|
+
status: row.status ?? null,
|
|
2534
|
+
currency: row.currency ?? null,
|
|
2535
|
+
amountCents: row.amountCents ?? null,
|
|
2536
|
+
amountPaidCents: row.amountPaidCents ?? null,
|
|
2537
|
+
amountRemainingCents: row.amountRemainingCents ?? null,
|
|
2538
|
+
customerId: row.customerId ?? null,
|
|
2539
|
+
subscriptionId: row.subscriptionId ?? null,
|
|
2540
|
+
invoiceId: row.invoiceId ?? null,
|
|
2541
|
+
priceId: row.priceId ?? null,
|
|
2542
|
+
quantity: row.quantity ?? null,
|
|
2543
|
+
applicationId: row.applicationId ?? null,
|
|
2544
|
+
personId: row.personId ?? null,
|
|
2545
|
+
membershipId: row.membershipId ?? null,
|
|
2546
|
+
currentPeriodStart: row.currentPeriodStart ?? null,
|
|
2547
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
2548
|
+
cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
|
|
2549
|
+
});
|
|
2550
|
+
var index = (projection2) => new Map(
|
|
2551
|
+
[...projection2.customers, ...projection2.subscriptions, ...projection2.invoices, ...projection2.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
|
|
2552
|
+
);
|
|
2553
|
+
function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
2554
|
+
const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
|
|
2555
|
+
const shadowRows = index(shadow);
|
|
2556
|
+
const stripeRows = index(stripe);
|
|
2557
|
+
const exceptions2 = [];
|
|
2558
|
+
const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
|
|
2559
|
+
if (!complete) exceptions2.push({ kind: "stripe_snapshot_truncated", detail: {} });
|
|
2560
|
+
for (const [key, row] of shadowRows) {
|
|
2561
|
+
const other = stripeRows.get(key);
|
|
2562
|
+
if (!other) {
|
|
2563
|
+
exceptions2.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
2564
|
+
} else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
|
|
2565
|
+
exceptions2.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
for (const [key, row] of stripeRows) {
|
|
2569
|
+
if (!shadowRows.has(key)) exceptions2.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
2570
|
+
}
|
|
2571
|
+
if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
|
|
2572
|
+
exceptions2.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
|
|
2573
|
+
}
|
|
2574
|
+
return {
|
|
2575
|
+
ok: exceptions2.length === 0,
|
|
2576
|
+
complete,
|
|
2577
|
+
ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
|
|
2578
|
+
exceptions: exceptions2,
|
|
2579
|
+
stripeTotals: stripe.totals
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2074
2583
|
// src/leader.ts
|
|
2075
2584
|
var import_crm3 = require("@odla-ai/crm");
|
|
2076
2585
|
function leaderCrmConfig(options = {}) {
|
|
@@ -2239,14 +2748,14 @@ var import_crm4 = require("@odla-ai/crm");
|
|
|
2239
2748
|
|
|
2240
2749
|
// src/tiers.ts
|
|
2241
2750
|
function buildTierSeeds(groupId, configured = [], now = 0) {
|
|
2242
|
-
return configured.map((tier,
|
|
2751
|
+
return configured.map((tier, index2) => ({
|
|
2243
2752
|
id: tier.id,
|
|
2244
2753
|
groupId,
|
|
2245
2754
|
name: tier.name,
|
|
2246
2755
|
priceCents: tier.priceCents,
|
|
2247
2756
|
...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
|
|
2248
2757
|
...tier.blurb ? { blurb: tier.blurb } : {},
|
|
2249
|
-
sortOrder: tier.sortOrder ??
|
|
2758
|
+
sortOrder: tier.sortOrder ?? index2,
|
|
2250
2759
|
active: tier.active !== false,
|
|
2251
2760
|
createdAt: now
|
|
2252
2761
|
}));
|
|
@@ -2319,13 +2828,26 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2319
2828
|
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
2320
2829
|
const runbooks = isLeader ? {
|
|
2321
2830
|
role: "leader",
|
|
2322
|
-
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
2831
|
+
slugs: ["chapter-leader", "chapter-network", "crm", ...chapter.networkCommercialReadModel ? ["stripe-network-shadow"] : []],
|
|
2323
2832
|
guidance: `${chapter.name} is a LEADER hub: it holds the CRM and shares records with its chapters. Read it with \`odla-ai runbook get chapter-leader\`.`
|
|
2324
2833
|
} : {
|
|
2325
2834
|
role: "follower",
|
|
2326
2835
|
slugs: ["chapter-follower", "chapter-network"],
|
|
2327
2836
|
guidance: `${chapter.name} is a FOLLOWER site: it receives people and records from a leader hub. Read it with \`odla-ai runbook get chapter-follower\`.`
|
|
2328
2837
|
};
|
|
2838
|
+
const secrets = [];
|
|
2839
|
+
if (chapter.account !== "none") secrets.push({
|
|
2840
|
+
// Released Chapter workers request this effective name. odla-db maps it to
|
|
2841
|
+
// the canonical reserved `$clerk_secret` value written by the CLI.
|
|
2842
|
+
name: "clerk_secret_key",
|
|
2843
|
+
description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
|
|
2844
|
+
required: true
|
|
2845
|
+
});
|
|
2846
|
+
if (chapter.networkCommercialReadModel) secrets.push({
|
|
2847
|
+
name: "network_commercial_stripe_webhook_secret",
|
|
2848
|
+
description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
|
|
2849
|
+
required: true
|
|
2850
|
+
});
|
|
2329
2851
|
return {
|
|
2330
2852
|
id: "chapter",
|
|
2331
2853
|
title: `Chapter: ${chapter.name}`,
|
|
@@ -2336,14 +2858,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2336
2858
|
},
|
|
2337
2859
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
2338
2860
|
seeds,
|
|
2339
|
-
secrets
|
|
2340
|
-
// Released Chapter workers request this effective name. odla-db maps
|
|
2341
|
-
// it to the canonical reserved `$clerk_secret` value written by
|
|
2342
|
-
// `odla-ai secrets set-clerk-key`.
|
|
2343
|
-
name: "clerk_secret_key",
|
|
2344
|
-
description: "Clerk backend key (set with odla-ai secrets set-clerk-key) required by the configured Chapter account model.",
|
|
2345
|
-
required: true
|
|
2346
|
-
}],
|
|
2861
|
+
secrets,
|
|
2347
2862
|
probes: [
|
|
2348
2863
|
...crmDesc.probes ?? [],
|
|
2349
2864
|
// The snapshot route is deliberately private. A credential-free smoke
|
|
@@ -2365,6 +2880,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2365
2880
|
{ path: "/api/builder-library-control", expectedStatus: 401 },
|
|
2366
2881
|
{ path: "/api/admin/builder-library", expectedStatus: 401 },
|
|
2367
2882
|
{ path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
|
|
2883
|
+
] : [],
|
|
2884
|
+
...chapter.networkPersonRegistry ? [{ path: "/api/admin/network-person-registry", expectedStatus: 401 }] : [],
|
|
2885
|
+
...chapter.networkCommercialReadModel ? [
|
|
2886
|
+
{ path: "/api/network-commercial/stripe-webhook", expectedStatus: 405 },
|
|
2887
|
+
{ path: "/api/admin/network-commercial-read-model", expectedStatus: 401 }
|
|
2368
2888
|
] : []
|
|
2369
2889
|
],
|
|
2370
2890
|
runbooks
|
|
@@ -2373,7 +2893,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2373
2893
|
|
|
2374
2894
|
// src/member-content-validation-helpers.ts
|
|
2375
2895
|
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2376
|
-
var
|
|
2896
|
+
var SLUG8 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2377
2897
|
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2378
2898
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2379
2899
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
@@ -2405,21 +2925,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
|
2405
2925
|
return valueText;
|
|
2406
2926
|
}
|
|
2407
2927
|
function offerValue(value, path, errors, tiers2, audience) {
|
|
2408
|
-
const
|
|
2409
|
-
if (!
|
|
2928
|
+
const raw2 = record(value);
|
|
2929
|
+
if (!raw2) {
|
|
2410
2930
|
errors.push(`${path} must be an object`);
|
|
2411
2931
|
return void 0;
|
|
2412
2932
|
}
|
|
2413
|
-
unknownKeys(
|
|
2414
|
-
const tierId = textValue(
|
|
2933
|
+
unknownKeys(raw2, OFFER_KEYS, path, errors);
|
|
2934
|
+
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2415
2935
|
if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2416
2936
|
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2417
2937
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2418
2938
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
2419
2939
|
}
|
|
2420
|
-
const label = textValue(
|
|
2421
|
-
const bodyMarkdown = textValue(
|
|
2422
|
-
const ctaLabel = textValue(
|
|
2940
|
+
const label = textValue(raw2.label, `${path}.label`, errors);
|
|
2941
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2942
|
+
const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2423
2943
|
return {
|
|
2424
2944
|
tierId,
|
|
2425
2945
|
...label ? { label } : {},
|
|
@@ -2471,37 +2991,37 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2471
2991
|
const ids = /* @__PURE__ */ new Set();
|
|
2472
2992
|
const slugs = /* @__PURE__ */ new Set();
|
|
2473
2993
|
const pages = [];
|
|
2474
|
-
for (const [
|
|
2475
|
-
const path = `document.pages[${
|
|
2476
|
-
const
|
|
2477
|
-
if (!
|
|
2994
|
+
for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2995
|
+
const path = `document.pages[${index2}]`;
|
|
2996
|
+
const raw2 = record(value);
|
|
2997
|
+
if (!raw2) {
|
|
2478
2998
|
errors.push(`${path} must be an object`);
|
|
2479
2999
|
continue;
|
|
2480
3000
|
}
|
|
2481
|
-
unknownKeys(
|
|
2482
|
-
const
|
|
2483
|
-
const slug = textValue(
|
|
2484
|
-
const title = textValue(
|
|
2485
|
-
if (
|
|
2486
|
-
if (slug && !
|
|
2487
|
-
if (ids.has(
|
|
3001
|
+
unknownKeys(raw2, PAGE_KEYS, path, errors);
|
|
3002
|
+
const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
3003
|
+
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
3004
|
+
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
3005
|
+
if (id6 && !ID.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3006
|
+
if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
3007
|
+
if (ids.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
|
|
2488
3008
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2489
|
-
ids.add(
|
|
3009
|
+
ids.add(id6);
|
|
2490
3010
|
slugs.add(slug);
|
|
2491
|
-
const navRaw = record(
|
|
2492
|
-
let navigation = { section: "General", order:
|
|
3011
|
+
const navRaw = record(raw2.navigation);
|
|
3012
|
+
let navigation = { section: "General", order: index2 };
|
|
2493
3013
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2494
3014
|
else {
|
|
2495
3015
|
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2496
3016
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2497
|
-
const
|
|
2498
|
-
if (!Number.isInteger(
|
|
3017
|
+
const order2 = navRaw.order;
|
|
3018
|
+
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
2499
3019
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2500
3020
|
}
|
|
2501
3021
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2502
|
-
navigation = { section, order: Number.isInteger(
|
|
3022
|
+
navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
|
|
2503
3023
|
}
|
|
2504
|
-
const audienceRaw = record(
|
|
3024
|
+
const audienceRaw = record(raw2.audience);
|
|
2505
3025
|
let audience = { kind: "all-members" };
|
|
2506
3026
|
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2507
3027
|
else {
|
|
@@ -2523,12 +3043,12 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2523
3043
|
audience = { kind: "tiers", tierIds: normalized };
|
|
2524
3044
|
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2525
3045
|
}
|
|
2526
|
-
const summary = textValue(
|
|
2527
|
-
const teaserMarkdown = textValue(
|
|
2528
|
-
const bodyMarkdown = textValue(
|
|
3046
|
+
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
3047
|
+
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
3048
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2529
3049
|
let upsell;
|
|
2530
|
-
if (
|
|
2531
|
-
const upsellRaw = record(
|
|
3050
|
+
if (raw2.upsell !== void 0) {
|
|
3051
|
+
const upsellRaw = record(raw2.upsell);
|
|
2532
3052
|
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2533
3053
|
else {
|
|
2534
3054
|
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
@@ -2542,13 +3062,13 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2542
3062
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2543
3063
|
}
|
|
2544
3064
|
}
|
|
2545
|
-
pages.push({ id:
|
|
3065
|
+
pages.push({ id: id6, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2546
3066
|
}
|
|
2547
3067
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2548
|
-
for (const [
|
|
3068
|
+
for (const [index2, page] of pages.entries()) {
|
|
2549
3069
|
const parentId = page.navigation.parentId;
|
|
2550
|
-
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${
|
|
2551
|
-
if (parentId === page.id) errors.push(`document.pages[${
|
|
3070
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
|
|
3071
|
+
if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
|
|
2552
3072
|
}
|
|
2553
3073
|
for (const page of pages) {
|
|
2554
3074
|
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
@@ -2569,21 +3089,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2569
3089
|
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2570
3090
|
const seen = /* @__PURE__ */ new Set();
|
|
2571
3091
|
upsellOverrides = [];
|
|
2572
|
-
for (const [
|
|
2573
|
-
const path = `document.upsellOverrides[${
|
|
2574
|
-
const
|
|
2575
|
-
if (!
|
|
3092
|
+
for (const [index2, value] of values.entries()) {
|
|
3093
|
+
const path = `document.upsellOverrides[${index2}]`;
|
|
3094
|
+
const raw2 = record(value);
|
|
3095
|
+
if (!raw2) {
|
|
2576
3096
|
errors.push(`${path} must be an object`);
|
|
2577
3097
|
continue;
|
|
2578
3098
|
}
|
|
2579
|
-
unknownKeys(
|
|
2580
|
-
const pageId = textValue(
|
|
3099
|
+
unknownKeys(raw2, OVERRIDE_KEYS, path, errors);
|
|
3100
|
+
const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2581
3101
|
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2582
3102
|
seen.add(pageId);
|
|
2583
3103
|
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2584
3104
|
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2585
3105
|
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2586
|
-
const offer = offerValue(
|
|
3106
|
+
const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2587
3107
|
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2588
3108
|
}
|
|
2589
3109
|
}
|
|
@@ -2708,7 +3228,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
2708
3228
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
2709
3229
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2710
3230
|
}
|
|
2711
|
-
function
|
|
3231
|
+
function text2(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
2712
3232
|
if (value === void 0 && !required) return void 0;
|
|
2713
3233
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
2714
3234
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -2718,23 +3238,23 @@ function text(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
|
2718
3238
|
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2719
3239
|
return result;
|
|
2720
3240
|
}
|
|
2721
|
-
function
|
|
3241
|
+
function integer2(value, path, errors, minimum = 1) {
|
|
2722
3242
|
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
2723
3243
|
return Number(value);
|
|
2724
3244
|
}
|
|
2725
3245
|
function scopeValue(value, path, errors) {
|
|
2726
|
-
const
|
|
2727
|
-
if (!
|
|
3246
|
+
const raw2 = record2(value);
|
|
3247
|
+
if (!raw2) {
|
|
2728
3248
|
errors.push(`${path} must be an object`);
|
|
2729
3249
|
return null;
|
|
2730
3250
|
}
|
|
2731
|
-
unknownKeys2(
|
|
2732
|
-
if (
|
|
2733
|
-
if (
|
|
2734
|
-
return { kind:
|
|
3251
|
+
unknownKeys2(raw2, SCOPE_KEYS, path, errors);
|
|
3252
|
+
if (raw2.kind === "global" || raw2.kind === "built_not_found") {
|
|
3253
|
+
if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
3254
|
+
return { kind: raw2.kind };
|
|
2735
3255
|
}
|
|
2736
|
-
if (
|
|
2737
|
-
const chapterId =
|
|
3256
|
+
if (raw2.kind === "chapter") {
|
|
3257
|
+
const chapterId = text2(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
2738
3258
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
2739
3259
|
return { kind: "chapter", chapterId };
|
|
2740
3260
|
}
|
|
@@ -2742,28 +3262,28 @@ function scopeValue(value, path, errors) {
|
|
|
2742
3262
|
return null;
|
|
2743
3263
|
}
|
|
2744
3264
|
function resourceValue(value, path, errors) {
|
|
2745
|
-
const
|
|
2746
|
-
if (!
|
|
3265
|
+
const raw2 = record2(value);
|
|
3266
|
+
if (!raw2) {
|
|
2747
3267
|
errors.push(`${path} must be an object`);
|
|
2748
3268
|
return null;
|
|
2749
3269
|
}
|
|
2750
|
-
unknownKeys2(
|
|
2751
|
-
const
|
|
2752
|
-
if (
|
|
2753
|
-
const kind =
|
|
3270
|
+
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
3271
|
+
const id6 = text2(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
3272
|
+
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
3273
|
+
const kind = text2(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
2754
3274
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
2755
|
-
const title =
|
|
2756
|
-
const summary =
|
|
2757
|
-
const status =
|
|
3275
|
+
const title = text2(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
3276
|
+
const summary = text2(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
3277
|
+
const status = text2(raw2.status, `${path}.status`, errors) ?? "";
|
|
2758
3278
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
2759
|
-
const revision =
|
|
2760
|
-
const scope = scopeValue(
|
|
2761
|
-
const tags = Array.isArray(
|
|
2762
|
-
if (!Array.isArray(
|
|
3279
|
+
const revision = integer2(raw2.revision, `${path}.revision`, errors);
|
|
3280
|
+
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
3281
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text2(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
3282
|
+
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
2763
3283
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
2764
3284
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
2765
|
-
const bodyMarkdown =
|
|
2766
|
-
const url =
|
|
3285
|
+
const bodyMarkdown = text2(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
3286
|
+
const url = text2(raw2.url, `${path}.url`, errors, 2048, false);
|
|
2767
3287
|
if (url) {
|
|
2768
3288
|
try {
|
|
2769
3289
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -2771,13 +3291,13 @@ function resourceValue(value, path, errors) {
|
|
|
2771
3291
|
errors.push(`${path}.url must be a valid https URL`);
|
|
2772
3292
|
}
|
|
2773
3293
|
}
|
|
2774
|
-
const asset =
|
|
2775
|
-
if (
|
|
3294
|
+
const asset = raw2.brandAsset === void 0 ? null : record2(raw2.brandAsset);
|
|
3295
|
+
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
2776
3296
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
2777
3297
|
const brandAsset = asset ? {
|
|
2778
|
-
bookId:
|
|
2779
|
-
assetId:
|
|
2780
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
3298
|
+
bookId: text2(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
3299
|
+
assetId: text2(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
3300
|
+
...asset.filename === void 0 ? {} : { filename: text2(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
2781
3301
|
} : void 0;
|
|
2782
3302
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
2783
3303
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -2785,59 +3305,59 @@ function resourceValue(value, path, errors) {
|
|
|
2785
3305
|
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
2786
3306
|
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
2787
3307
|
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
2788
|
-
const provenanceRaw = record2(
|
|
3308
|
+
const provenanceRaw = record2(raw2.provenance);
|
|
2789
3309
|
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
2790
3310
|
if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
2791
3311
|
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
|
|
2792
3312
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
2793
3313
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
2794
3314
|
const promotedFrom = promotedRaw ? {
|
|
2795
|
-
chapterId:
|
|
2796
|
-
resourceId:
|
|
2797
|
-
resourceRevision:
|
|
2798
|
-
documentRevision:
|
|
2799
|
-
promotedBy:
|
|
2800
|
-
promotedAt:
|
|
3315
|
+
chapterId: text2(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
3316
|
+
resourceId: text2(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
3317
|
+
resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
3318
|
+
documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
3319
|
+
promotedBy: text2(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
3320
|
+
promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
2801
3321
|
} : void 0;
|
|
2802
3322
|
const provenance = {
|
|
2803
|
-
createdBy:
|
|
2804
|
-
createdAt:
|
|
2805
|
-
updatedBy:
|
|
2806
|
-
updatedAt:
|
|
3323
|
+
createdBy: text2(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
3324
|
+
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
3325
|
+
updatedBy: text2(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
3326
|
+
updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
2807
3327
|
...promotedFrom ? { promotedFrom } : {}
|
|
2808
3328
|
};
|
|
2809
|
-
return { id:
|
|
3329
|
+
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
2810
3330
|
}
|
|
2811
3331
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
2812
3332
|
const errors = [];
|
|
2813
|
-
const
|
|
2814
|
-
if (!
|
|
2815
|
-
unknownKeys2(
|
|
2816
|
-
if (
|
|
2817
|
-
const revision =
|
|
2818
|
-
const generatedAt =
|
|
2819
|
-
const sourceRaw = record2(
|
|
3333
|
+
const raw2 = record2(value);
|
|
3334
|
+
if (!raw2) return { ok: false, errors: ["document must be an object"] };
|
|
3335
|
+
unknownKeys2(raw2, ROOT_KEYS2, "document", errors);
|
|
3336
|
+
if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
3337
|
+
const revision = integer2(raw2.revision, "document.revision", errors);
|
|
3338
|
+
const generatedAt = integer2(raw2.generatedAt, "document.generatedAt", errors, 0);
|
|
3339
|
+
const sourceRaw = record2(raw2.source);
|
|
2820
3340
|
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2821
3341
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
2822
3342
|
const sourceType = sourceRaw?.type;
|
|
2823
3343
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
2824
|
-
const sourceAppId =
|
|
3344
|
+
const sourceAppId = text2(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
2825
3345
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2826
3346
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2827
3347
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2828
|
-
const targetRaw =
|
|
2829
|
-
if (
|
|
3348
|
+
const targetRaw = raw2.target === void 0 ? null : record2(raw2.target);
|
|
3349
|
+
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
2830
3350
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
2831
3351
|
const target = targetRaw ? {
|
|
2832
|
-
appId:
|
|
2833
|
-
environment:
|
|
3352
|
+
appId: text2(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
3353
|
+
environment: text2(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
2834
3354
|
} : void 0;
|
|
2835
3355
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
2836
|
-
if (!Array.isArray(
|
|
2837
|
-
const inputs = Array.isArray(
|
|
3356
|
+
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
3357
|
+
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
2838
3358
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
2839
|
-
const resources = inputs.map((item,
|
|
2840
|
-
const ids = resources.map(({ id:
|
|
3359
|
+
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
3360
|
+
const ids = resources.map(({ id: id6 }) => id6);
|
|
2841
3361
|
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
2842
3362
|
for (const resource of resources) {
|
|
2843
3363
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -2954,10 +3474,10 @@ function planDelivery(input) {
|
|
|
2954
3474
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
2955
3475
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
2956
3476
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
2957
|
-
const
|
|
3477
|
+
const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
2958
3478
|
|
|
2959
3479
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
2960
|
-
return { deliver: true, transport, to, subject, text:
|
|
3480
|
+
return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
|
|
2961
3481
|
}
|
|
2962
3482
|
|
|
2963
3483
|
// src/notify.ts
|
|
@@ -2992,9 +3512,9 @@ async function sendTemplated(deps, input) {
|
|
|
2992
3512
|
error = e instanceof Error ? e.message : String(e);
|
|
2993
3513
|
}
|
|
2994
3514
|
}
|
|
2995
|
-
const
|
|
3515
|
+
const id6 = deps.newId();
|
|
2996
3516
|
const row = {
|
|
2997
|
-
id:
|
|
3517
|
+
id: id6,
|
|
2998
3518
|
groupId: input.group.id,
|
|
2999
3519
|
to: decision.to,
|
|
3000
3520
|
template: input.template,
|
|
@@ -3008,7 +3528,7 @@ async function sendTemplated(deps, input) {
|
|
|
3008
3528
|
...messageId ? { messageId } : {},
|
|
3009
3529
|
...error ? { error } : {}
|
|
3010
3530
|
};
|
|
3011
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
3531
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
3012
3532
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
3013
3533
|
}
|
|
3014
3534
|
function emailGroupFrom(row) {
|
|
@@ -3194,29 +3714,29 @@ function validateAdmissionGrantDocument(input) {
|
|
|
3194
3714
|
const grants = [];
|
|
3195
3715
|
const seenIds = /* @__PURE__ */ new Set();
|
|
3196
3716
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
3197
|
-
for (const [
|
|
3198
|
-
const grant =
|
|
3199
|
-
const
|
|
3717
|
+
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
3718
|
+
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
3719
|
+
const id6 = typeof grant.id === "string" ? grant.id : "";
|
|
3200
3720
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
3201
3721
|
const normalized = normalizeAdmissionEmail(email);
|
|
3202
3722
|
const decision = grant.decision;
|
|
3203
3723
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
3204
3724
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
3205
|
-
if (!ID3.test(
|
|
3206
|
-
if (seenIds.has(
|
|
3207
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${
|
|
3725
|
+
if (!ID3.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
3726
|
+
if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
|
|
3727
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
3208
3728
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
3209
3729
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
3210
|
-
errors.push(`document.grants[${
|
|
3730
|
+
errors.push(`document.grants[${index2}].decision is invalid`);
|
|
3211
3731
|
}
|
|
3212
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${
|
|
3732
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
|
|
3213
3733
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
3214
|
-
errors.push(`document.grants[${
|
|
3734
|
+
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
3215
3735
|
}
|
|
3216
|
-
seenIds.add(
|
|
3736
|
+
seenIds.add(id6);
|
|
3217
3737
|
seenEmails.add(normalized);
|
|
3218
3738
|
grants.push({
|
|
3219
|
-
id:
|
|
3739
|
+
id: id6,
|
|
3220
3740
|
email,
|
|
3221
3741
|
decision,
|
|
3222
3742
|
...note ? { note } : {},
|
|
@@ -3287,8 +3807,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3287
3807
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
3288
3808
|
const ops = [];
|
|
3289
3809
|
for (const row of current) {
|
|
3290
|
-
const
|
|
3291
|
-
if (
|
|
3810
|
+
const id6 = String(row.id ?? "");
|
|
3811
|
+
if (id6 && !incomingIds.has(id6)) ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
|
|
3292
3812
|
active: false,
|
|
3293
3813
|
revokedAt: receivedAt,
|
|
3294
3814
|
revokedBy: document.source.actor,
|
|
@@ -3297,10 +3817,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3297
3817
|
} });
|
|
3298
3818
|
}
|
|
3299
3819
|
for (const grant of document.grants) {
|
|
3300
|
-
const
|
|
3301
|
-
const existing = current.find((row) => row.id ===
|
|
3302
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
3303
|
-
id:
|
|
3820
|
+
const id6 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
3821
|
+
const existing = current.find((row) => row.id === id6);
|
|
3822
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id6, attrs: {
|
|
3823
|
+
id: id6,
|
|
3304
3824
|
sourceGrantId: grant.id,
|
|
3305
3825
|
sourceScope: "parent",
|
|
3306
3826
|
sourceId: document.source.appId,
|
|
@@ -3321,7 +3841,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
3321
3841
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
3322
3842
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
3323
3843
|
];
|
|
3324
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
3844
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id6, attrs: retract });
|
|
3325
3845
|
}
|
|
3326
3846
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
3327
3847
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -3363,7 +3883,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
3363
3883
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
3364
3884
|
var CURRENCY = /^[a-z]{3}$/;
|
|
3365
3885
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3366
|
-
var
|
|
3886
|
+
var text3 = (value, path, errors, max) => {
|
|
3367
3887
|
if (typeof value !== "string" || !value.trim()) {
|
|
3368
3888
|
errors.push(`${path} is required`);
|
|
3369
3889
|
return "";
|
|
@@ -3382,32 +3902,32 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3382
3902
|
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
3383
3903
|
const ids = /* @__PURE__ */ new Set();
|
|
3384
3904
|
const offers = [];
|
|
3385
|
-
for (const [
|
|
3386
|
-
const path = `document.giftOffers.offers[${
|
|
3387
|
-
const item = record3(
|
|
3905
|
+
for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
3906
|
+
const path = `document.giftOffers.offers[${index2}]`;
|
|
3907
|
+
const item = record3(raw2);
|
|
3388
3908
|
if (!item) {
|
|
3389
3909
|
errors.push(`${path} must be an object`);
|
|
3390
3910
|
continue;
|
|
3391
3911
|
}
|
|
3392
3912
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
3393
3913
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3394
|
-
const
|
|
3395
|
-
if (
|
|
3396
|
-
if (ids.has(
|
|
3397
|
-
ids.add(
|
|
3914
|
+
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
3915
|
+
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3916
|
+
if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
3917
|
+
ids.add(id6);
|
|
3398
3918
|
const targetRaw = record3(item.target);
|
|
3399
3919
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
3400
3920
|
if (targetRaw?.type === "tier") {
|
|
3401
|
-
const tierId =
|
|
3921
|
+
const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
3402
3922
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
3403
3923
|
target = { type: "tier", tierId };
|
|
3404
3924
|
} else if (targetRaw?.type === "entitlement") {
|
|
3405
|
-
const entitlementKey =
|
|
3925
|
+
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
3406
3926
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
3407
3927
|
target = { type: "entitlement", entitlementKey };
|
|
3408
3928
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
3409
3929
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
3410
|
-
const stripePriceId =
|
|
3930
|
+
const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
3411
3931
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
3412
3932
|
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
3413
3933
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -3420,7 +3940,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3420
3940
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
3421
3941
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
3422
3942
|
offers.push({
|
|
3423
|
-
id:
|
|
3943
|
+
id: id6,
|
|
3424
3944
|
target,
|
|
3425
3945
|
billingMode: item.billingMode,
|
|
3426
3946
|
stripePriceId,
|
|
@@ -3429,9 +3949,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3429
3949
|
redemptionLifetimeHours: lifetime,
|
|
3430
3950
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
3431
3951
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
3432
|
-
title:
|
|
3433
|
-
description:
|
|
3434
|
-
ctaLabel:
|
|
3952
|
+
title: text3(item.title, `${path}.title`, errors, 160),
|
|
3953
|
+
description: text3(item.description, `${path}.description`, errors, 2e3),
|
|
3954
|
+
ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
3435
3955
|
});
|
|
3436
3956
|
}
|
|
3437
3957
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -3471,8 +3991,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
3471
3991
|
const typeKey = type.toLowerCase();
|
|
3472
3992
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
3473
3993
|
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
3474
|
-
const
|
|
3475
|
-
if (readable &&
|
|
3994
|
+
const raw2 = `${prefix}:${hubRecordId}`;
|
|
3995
|
+
if (readable && raw2.length <= 64) return raw2;
|
|
3476
3996
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
3477
3997
|
}
|
|
3478
3998
|
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
@@ -3742,14 +4262,14 @@ function periodSeries(points, now, count, periodMs) {
|
|
|
3742
4262
|
for (const point of points) {
|
|
3743
4263
|
if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
|
|
3744
4264
|
if (point.t >= start && point.t <= now) {
|
|
3745
|
-
const
|
|
3746
|
-
current[
|
|
4265
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
|
|
4266
|
+
current[index2] = (current[index2] ?? 0) + point.v;
|
|
3747
4267
|
} else if (point.t >= previousStart && point.t < start) {
|
|
3748
|
-
const
|
|
3749
|
-
previous[
|
|
4268
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
|
|
4269
|
+
previous[index2] = (previous[index2] ?? 0) + point.v;
|
|
3750
4270
|
}
|
|
3751
4271
|
}
|
|
3752
|
-
const labels = current.map((_,
|
|
4272
|
+
const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
|
|
3753
4273
|
return { labels, current, previous };
|
|
3754
4274
|
}
|
|
3755
4275
|
function dashboardMetricData(points, now) {
|
|
@@ -3833,9 +4353,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
3833
4353
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
3834
4354
|
if (!found.ok) return false;
|
|
3835
4355
|
const users = await found.json().catch(() => null);
|
|
3836
|
-
const
|
|
3837
|
-
if (!
|
|
3838
|
-
return updateClerkUserMetadata(secretKey,
|
|
4356
|
+
const id6 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
4357
|
+
if (!id6) return false;
|
|
4358
|
+
return updateClerkUserMetadata(secretKey, id6, input, fetchImpl);
|
|
3839
4359
|
}
|
|
3840
4360
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
3841
4361
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -3898,8 +4418,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
3898
4418
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
3899
4419
|
return user ? toRecord(user) : null;
|
|
3900
4420
|
}
|
|
3901
|
-
async function clerkGetUser(secretKey,
|
|
3902
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
4421
|
+
async function clerkGetUser(secretKey, id6, fetchImpl = fetch) {
|
|
4422
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id6)}`, secretKey, fetchImpl).catch(() => null);
|
|
3903
4423
|
return data ? toRecord(data) : null;
|
|
3904
4424
|
}
|
|
3905
4425
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -3915,8 +4435,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
3915
4435
|
}
|
|
3916
4436
|
return out;
|
|
3917
4437
|
}
|
|
3918
|
-
async function clerkSetRole(secretKey,
|
|
3919
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
4438
|
+
async function clerkSetRole(secretKey, id6, role, fetchImpl = fetch) {
|
|
4439
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id6)}/metadata`, {
|
|
3920
4440
|
method: "PATCH",
|
|
3921
4441
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
3922
4442
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -4142,8 +4662,8 @@ function chapterBrandFromTokens(compiled, base = {}) {
|
|
|
4142
4662
|
function isReconcilable(meeting, now) {
|
|
4143
4663
|
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
4144
4664
|
}
|
|
4145
|
-
function reconcileMeetings(meetings2,
|
|
4146
|
-
const byEvent = new Map(
|
|
4665
|
+
function reconcileMeetings(meetings2, events2, now) {
|
|
4666
|
+
const byEvent = new Map(events2.map((e) => [e.eventId, e]));
|
|
4147
4667
|
const decisions = [];
|
|
4148
4668
|
for (const m of meetings2) {
|
|
4149
4669
|
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
@@ -4353,10 +4873,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
4353
4873
|
errors.push(`${path} must be a string`);
|
|
4354
4874
|
return void 0;
|
|
4355
4875
|
}
|
|
4356
|
-
const
|
|
4357
|
-
if (required && !
|
|
4358
|
-
if (
|
|
4359
|
-
return
|
|
4876
|
+
const text5 = value.trim();
|
|
4877
|
+
if (required && !text5) errors.push(`${path} is required`);
|
|
4878
|
+
if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
4879
|
+
return text5;
|
|
4360
4880
|
}
|
|
4361
4881
|
function validateMembershipPageDocument(input, options = {}) {
|
|
4362
4882
|
const errors = [];
|
|
@@ -4374,9 +4894,9 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4374
4894
|
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
4375
4895
|
const seen = /* @__PURE__ */ new Set();
|
|
4376
4896
|
const tiers2 = [];
|
|
4377
|
-
for (const [
|
|
4378
|
-
const path = `document.tiers[${
|
|
4379
|
-
const item = objectRecord(
|
|
4897
|
+
for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
4898
|
+
const path = `document.tiers[${index2}]`;
|
|
4899
|
+
const item = objectRecord(raw2);
|
|
4380
4900
|
if (!item) {
|
|
4381
4901
|
errors.push(`${path} must be an object`);
|
|
4382
4902
|
continue;
|
|
@@ -4405,8 +4925,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4405
4925
|
else {
|
|
4406
4926
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
4407
4927
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
4408
|
-
const
|
|
4409
|
-
return
|
|
4928
|
+
const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
4929
|
+
return text5 ? [text5] : [];
|
|
4410
4930
|
});
|
|
4411
4931
|
}
|
|
4412
4932
|
}
|
|
@@ -4482,7 +5002,7 @@ function membershipPageDocument(input, options = {}) {
|
|
|
4482
5002
|
}
|
|
4483
5003
|
|
|
4484
5004
|
// src/signup-control-validate.ts
|
|
4485
|
-
var
|
|
5005
|
+
var SLUG9 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4486
5006
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
4487
5007
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
4488
5008
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
@@ -4505,7 +5025,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
4505
5025
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
4506
5026
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
4507
5027
|
}
|
|
4508
|
-
function
|
|
5028
|
+
function text4(value, path, errors, max = 65536) {
|
|
4509
5029
|
if (typeof value !== "string" || !value.trim()) {
|
|
4510
5030
|
errors.push(`${path} is required`);
|
|
4511
5031
|
return "";
|
|
@@ -4520,19 +5040,19 @@ function tiersOf(value, errors) {
|
|
|
4520
5040
|
}
|
|
4521
5041
|
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
4522
5042
|
const seen = /* @__PURE__ */ new Set();
|
|
4523
|
-
return value.slice(0, 20).flatMap((
|
|
4524
|
-
const path = `document.tiers[${
|
|
4525
|
-
const item = recordOf(
|
|
5043
|
+
return value.slice(0, 20).flatMap((raw2, index2) => {
|
|
5044
|
+
const path = `document.tiers[${index2}]`;
|
|
5045
|
+
const item = recordOf(raw2);
|
|
4526
5046
|
if (!item) {
|
|
4527
5047
|
errors.push(`${path} must be an object`);
|
|
4528
5048
|
return [];
|
|
4529
5049
|
}
|
|
4530
5050
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
4531
|
-
const
|
|
4532
|
-
if (
|
|
4533
|
-
if (seen.has(
|
|
4534
|
-
seen.add(
|
|
4535
|
-
const name =
|
|
5051
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
5052
|
+
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5053
|
+
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5054
|
+
seen.add(id6);
|
|
5055
|
+
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
4536
5056
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
4537
5057
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
4538
5058
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -4544,9 +5064,9 @@ function tiersOf(value, errors) {
|
|
|
4544
5064
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
4545
5065
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
4546
5066
|
}
|
|
4547
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
5067
|
+
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
4548
5068
|
return [{
|
|
4549
|
-
id:
|
|
5069
|
+
id: id6,
|
|
4550
5070
|
name,
|
|
4551
5071
|
available: item.available === true,
|
|
4552
5072
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -4600,7 +5120,7 @@ function policyOf(value, errors) {
|
|
|
4600
5120
|
const result = {};
|
|
4601
5121
|
for (const key of allowed2) {
|
|
4602
5122
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
4603
|
-
result[key] =
|
|
5123
|
+
result[key] = text4(input[key], `document.policy.${key}`, errors);
|
|
4604
5124
|
}
|
|
4605
5125
|
return result;
|
|
4606
5126
|
}
|
|
@@ -4638,18 +5158,18 @@ function validateSignupControlDocument(input) {
|
|
|
4638
5158
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
4639
5159
|
const source = recordOf(root.source);
|
|
4640
5160
|
const target = recordOf(root.target);
|
|
4641
|
-
const appId =
|
|
4642
|
-
const chapterId =
|
|
4643
|
-
const environment =
|
|
4644
|
-
if (appId && !
|
|
4645
|
-
if (chapterId && !
|
|
5161
|
+
const appId = text4(source?.appId, "document.source.appId", errors, 63);
|
|
5162
|
+
const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
5163
|
+
const environment = text4(target?.environment, "document.target.environment", errors, 63);
|
|
5164
|
+
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
5165
|
+
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
4646
5166
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
4647
5167
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
4648
5168
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
4649
5169
|
const available = tiers2.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
4650
5170
|
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
4651
5171
|
if (!page.ok) errors.push(...page.errors);
|
|
4652
|
-
else if (page.value.tiers.some((tier,
|
|
5172
|
+
else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
|
|
4653
5173
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
4654
5174
|
}
|
|
4655
5175
|
const application = applicationOf(root.application, errors);
|
|
@@ -4696,14 +5216,14 @@ function validatedTokens(input) {
|
|
|
4696
5216
|
}
|
|
4697
5217
|
const tokens = {};
|
|
4698
5218
|
let serializedLength = 0;
|
|
4699
|
-
for (const [name,
|
|
5219
|
+
for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
4700
5220
|
if (!TOKEN_NAME.test(name)) {
|
|
4701
5221
|
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
4702
5222
|
}
|
|
4703
|
-
if (typeof
|
|
5223
|
+
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
4704
5224
|
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
4705
5225
|
}
|
|
4706
|
-
const value = String(
|
|
5226
|
+
const value = String(raw2).trim();
|
|
4707
5227
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
4708
5228
|
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
4709
5229
|
}
|
|
@@ -4810,16 +5330,16 @@ function markdownSection(rawHeading) {
|
|
|
4810
5330
|
}
|
|
4811
5331
|
function markdownBenefits(lines, path, errors) {
|
|
4812
5332
|
const benefits = [];
|
|
4813
|
-
for (const [
|
|
5333
|
+
for (const [index2, line] of lines.entries()) {
|
|
4814
5334
|
if (!line.trim()) continue;
|
|
4815
5335
|
const match = line.match(MARKDOWN_BENEFIT);
|
|
4816
5336
|
if (!match) {
|
|
4817
|
-
errors.push(`${path} line ${
|
|
5337
|
+
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
4818
5338
|
continue;
|
|
4819
5339
|
}
|
|
4820
|
-
const
|
|
4821
|
-
if (
|
|
4822
|
-
if (
|
|
5340
|
+
const text5 = (match[1] ?? "").trim();
|
|
5341
|
+
if (text5.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
5342
|
+
if (text5) benefits.push(text5);
|
|
4823
5343
|
}
|
|
4824
5344
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
4825
5345
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -4834,7 +5354,7 @@ function applyMembershipTierMarkdown(presentation, source) {
|
|
|
4834
5354
|
const sections = /* @__PURE__ */ new Map();
|
|
4835
5355
|
const prelude = [];
|
|
4836
5356
|
let current = null;
|
|
4837
|
-
for (const [
|
|
5357
|
+
for (const [index2, line] of source.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
4838
5358
|
const heading = line.match(MARKDOWN_SECTION);
|
|
4839
5359
|
if (heading) {
|
|
4840
5360
|
const headingText = heading[1] ?? "";
|
|
@@ -4854,7 +5374,7 @@ function applyMembershipTierMarkdown(presentation, source) {
|
|
|
4854
5374
|
continue;
|
|
4855
5375
|
}
|
|
4856
5376
|
if (MARKDOWN_HEADING.test(line)) {
|
|
4857
|
-
errors.push(`membership Markdown line ${
|
|
5377
|
+
errors.push(`membership Markdown line ${index2 + 1} uses an unsupported heading`);
|
|
4858
5378
|
continue;
|
|
4859
5379
|
}
|
|
4860
5380
|
if (current) sections.get(current)?.lines.push(line);
|
|
@@ -4919,7 +5439,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
4919
5439
|
id: chapterId,
|
|
4920
5440
|
reason: "provider_identity",
|
|
4921
5441
|
paths: [
|
|
4922
|
-
...paidTierIds.map((
|
|
5442
|
+
...paidTierIds.map((id6) => `tiers[${id6}].stripePriceId`),
|
|
4923
5443
|
"groups.stripePublishableKey",
|
|
4924
5444
|
"applications.stripeCustomerId",
|
|
4925
5445
|
"applications.stripeSubscriptionId",
|
|
@@ -4961,9 +5481,9 @@ async function exportChapterPromotion(input) {
|
|
|
4961
5481
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
4962
5482
|
receiptDigests: [input.digest]
|
|
4963
5483
|
};
|
|
4964
|
-
const artifact = (kind,
|
|
5484
|
+
const artifact = (kind, id6, content) => ({
|
|
4965
5485
|
kind,
|
|
4966
|
-
id:
|
|
5486
|
+
id: id6,
|
|
4967
5487
|
sourceRevision: revision,
|
|
4968
5488
|
provenance: promotionProvenance,
|
|
4969
5489
|
content: (0, import_promotion.canonicalPromotionValue)(content)
|
|
@@ -5002,7 +5522,7 @@ async function exportChapterPromotion(input) {
|
|
|
5002
5522
|
}
|
|
5003
5523
|
|
|
5004
5524
|
// src/member-system-authority.ts
|
|
5005
|
-
var rows2 = (group) => group.fields.map((
|
|
5525
|
+
var rows2 = (group) => group.fields.map((id6) => ({ ...group, id: id6 }));
|
|
5006
5526
|
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
5007
5527
|
...rows2({
|
|
5008
5528
|
scope: "network",
|
|
@@ -5387,10 +5907,10 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
5387
5907
|
if (supplied.length === keys.length) return "apply";
|
|
5388
5908
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
5389
5909
|
}
|
|
5390
|
-
function planMemberSystemEventReplay(
|
|
5910
|
+
function planMemberSystemEventReplay(events2) {
|
|
5391
5911
|
const seen = /* @__PURE__ */ new Map();
|
|
5392
5912
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
5393
|
-
for (const event of
|
|
5913
|
+
for (const event of events2) {
|
|
5394
5914
|
const digest = seen.get(event.id);
|
|
5395
5915
|
if (digest === void 0) {
|
|
5396
5916
|
seen.set(event.id, event.digest);
|