@odla-ai/chapter 0.62.0 → 0.63.1
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 +61 -0
- package/dist/{chunk-7CVP6VSO.js → chunk-OXYMVK2O.js} +145 -1
- package/dist/chunk-OXYMVK2O.js.map +1 -0
- package/dist/{chunk-MUUBATZJ.js → chunk-S6VPJFHL.js} +474 -200
- package/dist/chunk-S6VPJFHL.js.map +1 -0
- package/dist/{chunk-ZIYK3OV2.js → chunk-XFD66VFP.js} +697 -338
- package/dist/chunk-XFD66VFP.js.map +1 -0
- package/dist/{copy-context-BJUkVUjE.d.ts → copy-context-BNKK7ZDm.d.ts} +6 -0
- package/dist/index.cjs +419 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -6
- package/dist/index.d.ts +196 -6
- package/dist/index.js +419 -73
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +53 -11
- package/dist/ui/member/index.d.ts +175 -3
- package/dist/ui/member/index.js +52 -10
- package/dist/worker/index.cjs +3722 -2903
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +6 -0
- package/dist/worker/index.d.ts +6 -0
- package/dist/worker/index.js +3729 -2910
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7CVP6VSO.js.map +0 -1
- package/dist/chunk-MUUBATZJ.js.map +0 -1
- package/dist/chunk-ZIYK3OV2.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -55,6 +55,7 @@ __export(src_exports, {
|
|
|
55
55
|
MEMBER_PHOTO_PATH: () => MEMBER_PHOTO_PATH,
|
|
56
56
|
MEMBER_PROFILE_FIELDS: () => MEMBER_PROFILE_FIELDS,
|
|
57
57
|
MEMBER_PROFILE_PATH: () => MEMBER_PROFILE_PATH,
|
|
58
|
+
MEMBER_REFERRALS_PATH: () => MEMBER_REFERRALS_PATH,
|
|
58
59
|
MEMBER_SELF_EDITABLE_FIELDS: () => MEMBER_SELF_EDITABLE_FIELDS,
|
|
59
60
|
MEMBER_SELF_READABLE_FIELDS: () => MEMBER_SELF_READABLE_FIELDS,
|
|
60
61
|
MEMBER_SYSTEM_AUTHORITY_MATRIX: () => MEMBER_SYSTEM_AUTHORITY_MATRIX,
|
|
@@ -62,6 +63,9 @@ __export(src_exports, {
|
|
|
62
63
|
MEMBER_SYSTEM_RECORD_OWNERS: () => MEMBER_SYSTEM_RECORD_OWNERS,
|
|
63
64
|
MEMBER_SYSTEM_RECOVERY_OBJECTIVES: () => MEMBER_SYSTEM_RECOVERY_OBJECTIVES,
|
|
64
65
|
PHOTO_SIZE: () => PHOTO_SIZE,
|
|
66
|
+
REFERRAL_CODE_ALPHABET: () => REFERRAL_CODE_ALPHABET,
|
|
67
|
+
REFERRAL_CODE_PATTERN: () => REFERRAL_CODE_PATTERN,
|
|
68
|
+
REFERRAL_COOKIE: () => REFERRAL_COOKIE,
|
|
65
69
|
REQUIRED_NOTICE_KINDS: () => REQUIRED_NOTICE_KINDS,
|
|
66
70
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
67
71
|
SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
|
|
@@ -116,6 +120,7 @@ __export(src_exports, {
|
|
|
116
120
|
composeMemberContent: () => composeMemberContent,
|
|
117
121
|
composeMemberSections: () => composeMemberSections,
|
|
118
122
|
confirmMembershipPayment: () => confirmMembershipPayment,
|
|
123
|
+
courseEventInput: () => courseEventInput,
|
|
119
124
|
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
120
125
|
createBuilderLibraryEnvelope: () => createBuilderLibraryEnvelope,
|
|
121
126
|
createChapterIntegration: () => createChapterIntegration,
|
|
@@ -136,12 +141,15 @@ __export(src_exports, {
|
|
|
136
141
|
emailSenderWarnings: () => emailSenderWarnings,
|
|
137
142
|
emailSpamSignals: () => emailSpamSignals,
|
|
138
143
|
endForSlot: () => endForSlot,
|
|
144
|
+
ensureReferralCode: () => ensureReferralCode,
|
|
145
|
+
entitledTierId: () => entitledTierId,
|
|
139
146
|
exportChapterPromotion: () => exportChapterPromotion,
|
|
140
147
|
findAdmissionGrant: () => findAdmissionGrant,
|
|
141
148
|
findApplicationRef: () => findApplicationRef,
|
|
142
149
|
findGiftOffer: () => findGiftOffer,
|
|
143
150
|
findTier: () => findTier,
|
|
144
151
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
152
|
+
foldLedger: () => foldLedger,
|
|
145
153
|
formatChapterCopy: () => formatChapterCopy,
|
|
146
154
|
formationFields: () => formationFields,
|
|
147
155
|
formationFlowFields: () => formationFlowFields,
|
|
@@ -175,6 +183,7 @@ __export(src_exports, {
|
|
|
175
183
|
memberApplication: () => memberApplication,
|
|
176
184
|
memberContentDigest: () => memberContentDigest,
|
|
177
185
|
memberContentDocument: () => memberContentDocument,
|
|
186
|
+
memberCourseView: () => memberCourseView,
|
|
178
187
|
memberFeatureAccess: () => memberFeatureAccess,
|
|
179
188
|
memberPageAllowed: () => memberPageAllowed,
|
|
180
189
|
memberPortalEntities: () => memberPortalEntities,
|
|
@@ -191,12 +200,14 @@ __export(src_exports, {
|
|
|
191
200
|
membershipOfferRules: () => membershipOfferRules,
|
|
192
201
|
membershipOfferingVersion: () => membershipOfferingVersion,
|
|
193
202
|
membershipPageDocument: () => membershipPageDocument,
|
|
203
|
+
membershipRedirect: () => membershipRedirect,
|
|
194
204
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
195
205
|
nameKey: () => nameKey,
|
|
196
206
|
namedSeatOfferDigest: () => namedSeatOfferDigest,
|
|
197
207
|
networkCommercialSourceKey: () => networkCommercialSourceKey,
|
|
198
208
|
networkMembershipId: () => networkMembershipId,
|
|
199
209
|
networkSourceTag: () => networkSourceTag,
|
|
210
|
+
newReferralCode: () => newReferralCode,
|
|
200
211
|
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
201
212
|
normalizeChapterEmail: () => normalizeChapterEmail,
|
|
202
213
|
normalizeNetworkCommercialStripeEvent: () => normalizeNetworkCommercialStripeEvent,
|
|
@@ -230,8 +241,10 @@ __export(src_exports, {
|
|
|
230
241
|
reconcileMembershipBilling: () => reconcileMembershipBilling,
|
|
231
242
|
reconcileMembershipRenewal: () => reconcileMembershipRenewal,
|
|
232
243
|
reconcileStripeSnapshot: () => reconcileStripeSnapshot,
|
|
244
|
+
referralCodeFromCookie: () => referralCodeFromCookie,
|
|
233
245
|
refundNetworkMembership: () => refundNetworkMembership,
|
|
234
246
|
refundedPatch: () => refundedPatch,
|
|
247
|
+
reissueReferralCode: () => reissueReferralCode,
|
|
235
248
|
render: () => render,
|
|
236
249
|
renderHtmlBody: () => renderHtmlBody,
|
|
237
250
|
renderSignupControlThemeHead: () => renderSignupControlThemeHead,
|
|
@@ -297,6 +310,7 @@ __export(src_exports, {
|
|
|
297
310
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
298
311
|
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
299
312
|
validateBuilderLibraryDocument: () => validateBuilderLibraryDocument,
|
|
313
|
+
validateCourseSettings: () => validateCourseSettings,
|
|
300
314
|
validateEmailReadiness: () => validateEmailReadiness,
|
|
301
315
|
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
302
316
|
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
@@ -597,7 +611,10 @@ var applicationAttrs = {
|
|
|
597
611
|
namedSeatAcceptedAt: attr("number", { optional: true }),
|
|
598
612
|
approvedAt: attr("number", { optional: true }),
|
|
599
613
|
entitlementTierId: attr("string", { optional: true, indexed: true }),
|
|
600
|
-
entitlementTierEndsAt: attr("number", { optional: true })
|
|
614
|
+
entitlementTierEndsAt: attr("number", { optional: true }),
|
|
615
|
+
referredByMemberId: attr("string", { optional: true, indexed: true }),
|
|
616
|
+
referralCode: attr("string", { optional: true }),
|
|
617
|
+
referredAt: attr("number", { optional: true })
|
|
601
618
|
};
|
|
602
619
|
function applicationsFor(application) {
|
|
603
620
|
const attrs = {};
|
|
@@ -2039,6 +2056,89 @@ var DEFAULT_PORTAL_COPY = {
|
|
|
2039
2056
|
saved: "Saved",
|
|
2040
2057
|
conflict: "Your profile changed in another window. Reload to see the latest version.",
|
|
2041
2058
|
saveFailed: "Your profile could not be saved. Please try again."
|
|
2059
|
+
},
|
|
2060
|
+
share: {
|
|
2061
|
+
eyebrow: "Community",
|
|
2062
|
+
intro: "Invite the people you would like to have beside you.",
|
|
2063
|
+
officialTitle: "Make it official on LinkedIn",
|
|
2064
|
+
officialBody: "Add your membership to your LinkedIn profile. It takes two minutes.",
|
|
2065
|
+
showMeHow: "Show me how",
|
|
2066
|
+
stepsEyebrow: "Two minutes, three steps",
|
|
2067
|
+
steps: [
|
|
2068
|
+
"On your profile, select Add profile section, then Add position.",
|
|
2069
|
+
"Title: {tierName}. Company: choose us from the list.",
|
|
2070
|
+
"Paste the description into the description box and save."
|
|
2071
|
+
],
|
|
2072
|
+
openLinkedIn: "Open my LinkedIn profile",
|
|
2073
|
+
copyDescription: "Copy description",
|
|
2074
|
+
linkedinDescription: "{tierName}.",
|
|
2075
|
+
postTitle: "Post about us",
|
|
2076
|
+
postBody: "Tell your network you are here. The post is already written.",
|
|
2077
|
+
shareOnLinkedIn: "Share on LinkedIn",
|
|
2078
|
+
previewPost: "Preview the post",
|
|
2079
|
+
linkedinPost: "I'm proud to be {article} {tierName}.",
|
|
2080
|
+
linkTitle: "Your personal invite link",
|
|
2081
|
+
linkBody: "Send it to anyone you would like beside you.",
|
|
2082
|
+
linkCredit: "When a friend joins as a paying member with your link, {credit} comes off your next year's membership.",
|
|
2083
|
+
referralsCredit: "Each friend who joins as a paying member takes {credit} off your next year's membership.",
|
|
2084
|
+
copyLink: "Copy link",
|
|
2085
|
+
copied: "Copied!",
|
|
2086
|
+
copyBlocked: "Copy not allowed here",
|
|
2087
|
+
emailInvite: "Email an invitation",
|
|
2088
|
+
textInvite: "Text an invitation",
|
|
2089
|
+
inviteNote: "Each opens your email or messages with a note already written. You choose who it goes to.",
|
|
2090
|
+
emailSubject: "Join me",
|
|
2091
|
+
emailBody: "Hi,\n\nI thought of you. Here's my personal invite link:\n{inviteUrl}\n\n{firstName}",
|
|
2092
|
+
textBody: "Here's my invite link if you're curious: {inviteUrl}",
|
|
2093
|
+
fallbackEmail: "Your email is ready. If your mail app didn't open, copy the message and paste it into an email.",
|
|
2094
|
+
fallbackText: "Your text is ready. If your messages app didn't open, copy the message and paste it into a text.",
|
|
2095
|
+
copyMessage: "Copy message",
|
|
2096
|
+
close: "Close",
|
|
2097
|
+
referralsTitle: "Your referrals",
|
|
2098
|
+
referralsBody: "People who joined with your personal invite link.",
|
|
2099
|
+
inviteFriend: "Invite a friend",
|
|
2100
|
+
joined: "Joined",
|
|
2101
|
+
inProcess: "In process",
|
|
2102
|
+
offNextYear: "Off next year",
|
|
2103
|
+
joinedWith: "Joined with your link: {names}"
|
|
2104
|
+
},
|
|
2105
|
+
course: {
|
|
2106
|
+
eyebrow: "Live course \xB7 Registration open",
|
|
2107
|
+
reserve: "Reserve my seat",
|
|
2108
|
+
seeDetails: "See details",
|
|
2109
|
+
courseDetails: "Course details",
|
|
2110
|
+
youreIn: "You're in",
|
|
2111
|
+
invitesOnWay: "Calendar invitations for every session are on their way, each with the Google Meet link.",
|
|
2112
|
+
openMeet: "Open Google Meet",
|
|
2113
|
+
giveUp: "Give up my seat",
|
|
2114
|
+
confirmGiveUp: "Give up your seat? Your calendar invitations will be canceled.",
|
|
2115
|
+
joinWaitlist: "Join the waitlist",
|
|
2116
|
+
onWaitlist: "On the waitlist, number {position}",
|
|
2117
|
+
leaveWaitlist: "Leave the waitlist",
|
|
2118
|
+
seatsOpen: "{left} of {capacity} seats open",
|
|
2119
|
+
full: "The course is full",
|
|
2120
|
+
closed: "Sign-up has closed",
|
|
2121
|
+
when: "When",
|
|
2122
|
+
dates: "Dates",
|
|
2123
|
+
where: "Where",
|
|
2124
|
+
seats: "Seats",
|
|
2125
|
+
googleMeet: "Google Meet",
|
|
2126
|
+
inviteEvery: "We'll send a calendar invitation for every session.",
|
|
2127
|
+
guidesTitle: "Your guides",
|
|
2128
|
+
sessionsTitle: "Sessions",
|
|
2129
|
+
materialsOpen: "Open a session for what to watch, read, and do beforehand.",
|
|
2130
|
+
materialsLocked: "Materials open when you reserve your seat.",
|
|
2131
|
+
items: "{count} items",
|
|
2132
|
+
outcomesTitle: "You'll leave with",
|
|
2133
|
+
toolkitTitle: "Your toolkit",
|
|
2134
|
+
timeTitle: "Time",
|
|
2135
|
+
open: "Open",
|
|
2136
|
+
read: "Read",
|
|
2137
|
+
do: "Do",
|
|
2138
|
+
watch: "Watch",
|
|
2139
|
+
extra: "Optional",
|
|
2140
|
+
failed: "Something went wrong. Please try again.",
|
|
2141
|
+
reserveYourSeat: "Reserve your seat"
|
|
2042
2142
|
}
|
|
2043
2143
|
};
|
|
2044
2144
|
|
|
@@ -2098,8 +2198,8 @@ var APPLICATION_LABELS = {
|
|
|
2098
2198
|
state: "State"
|
|
2099
2199
|
};
|
|
2100
2200
|
function humanize(id12) {
|
|
2101
|
-
const
|
|
2102
|
-
return
|
|
2201
|
+
const text9 = id12.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
2202
|
+
return text9 ? `${text9[0].toUpperCase()}${text9.slice(1)}` : id12;
|
|
2103
2203
|
}
|
|
2104
2204
|
function applicationDefs(chapter) {
|
|
2105
2205
|
try {
|
|
@@ -4694,24 +4794,24 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
4694
4794
|
let earliest;
|
|
4695
4795
|
const seen = /* @__PURE__ */ new Set();
|
|
4696
4796
|
for (let page = 0; page < 10; page++) {
|
|
4697
|
-
const
|
|
4797
|
+
const list2 = await call("GET", "/v1/invoices", {
|
|
4698
4798
|
subscription: input.subscriptionId,
|
|
4699
4799
|
customer: input.customerId,
|
|
4700
4800
|
status: "paid",
|
|
4701
4801
|
limit: 100,
|
|
4702
4802
|
...cursor ? { starting_after: cursor } : {}
|
|
4703
4803
|
});
|
|
4704
|
-
if (!Array.isArray(
|
|
4705
|
-
for (const value of
|
|
4804
|
+
if (!Array.isArray(list2.data) || typeof list2.has_more !== "boolean") fail3("stripe_invalid_response");
|
|
4805
|
+
for (const value of list2.data) {
|
|
4706
4806
|
const invoice2 = row(value);
|
|
4707
4807
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
4708
4808
|
if (!providerId2(id8(invoice2), "in") || seen.has(id8(invoice2)) || id8(subscription) !== input.subscriptionId || id8(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail3();
|
|
4709
4809
|
seen.add(id8(invoice2));
|
|
4710
4810
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
4711
4811
|
}
|
|
4712
|
-
if (!
|
|
4713
|
-
if (!
|
|
4714
|
-
cursor = id8(
|
|
4812
|
+
if (!list2.has_more) break;
|
|
4813
|
+
if (!list2.data.length || page === 9) fail3("refund_history_incomplete");
|
|
4814
|
+
cursor = id8(list2.data[list2.data.length - 1]);
|
|
4715
4815
|
}
|
|
4716
4816
|
if (!earliest) fail3("no_charge");
|
|
4717
4817
|
const invoice = earliest;
|
|
@@ -5947,11 +6047,11 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
|
5947
6047
|
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
5948
6048
|
}
|
|
5949
6049
|
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
5950
|
-
const
|
|
5951
|
-
if (!Array.isArray(
|
|
6050
|
+
const list2 = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
6051
|
+
if (!Array.isArray(list2.data) || list2.has_more !== false || list2.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
5952
6052
|
let refundedCents = 0;
|
|
5953
6053
|
const refundIds = [];
|
|
5954
|
-
for (const refund of
|
|
6054
|
+
for (const refund of list2.data) {
|
|
5955
6055
|
if (id9(refund.charge) !== chargeId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !/^re_[A-Za-z0-9_]+$/.test(refund.id) || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
|
|
5956
6056
|
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
5957
6057
|
if (refund.status === "succeeded") {
|
|
@@ -6002,11 +6102,11 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
6002
6102
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
6003
6103
|
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
6004
6104
|
}
|
|
6005
|
-
const
|
|
6006
|
-
if (!Array.isArray(
|
|
6105
|
+
const list2 = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
6106
|
+
if (!Array.isArray(list2.data) || list2.has_more !== false || list2.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
6007
6107
|
let refundedCents = 0, pendingCents = 0;
|
|
6008
6108
|
const refundIds = [];
|
|
6009
|
-
for (const refund of
|
|
6109
|
+
for (const refund of list2.data) {
|
|
6010
6110
|
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail5();
|
|
6011
6111
|
if (refund.status === "succeeded") {
|
|
6012
6112
|
refundedCents += refund.amount;
|
|
@@ -6687,7 +6787,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6687
6787
|
};
|
|
6688
6788
|
signal.addEventListener("abort", cancel, { once: true });
|
|
6689
6789
|
let size = 0;
|
|
6690
|
-
let
|
|
6790
|
+
let text9 = "";
|
|
6691
6791
|
const decoder = new TextDecoder();
|
|
6692
6792
|
try {
|
|
6693
6793
|
while (true) {
|
|
@@ -6697,10 +6797,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6697
6797
|
if (done) break;
|
|
6698
6798
|
size += value.byteLength;
|
|
6699
6799
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
6700
|
-
|
|
6800
|
+
text9 += decoder.decode(value, { stream: true });
|
|
6701
6801
|
}
|
|
6702
6802
|
try {
|
|
6703
|
-
return JSON.parse(
|
|
6803
|
+
return JSON.parse(text9 + decoder.decode());
|
|
6704
6804
|
} catch {
|
|
6705
6805
|
throw new TypeError(`invalid ${label}`);
|
|
6706
6806
|
}
|
|
@@ -8087,6 +8187,14 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
8087
8187
|
return { document, digest: await builderLibraryDigest(document) };
|
|
8088
8188
|
}
|
|
8089
8189
|
|
|
8190
|
+
// src/member-entitlement.ts
|
|
8191
|
+
function entitledTierId(app, now = Date.now()) {
|
|
8192
|
+
const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
|
|
8193
|
+
const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
|
|
8194
|
+
if (granted && (endsAt === null || endsAt > now)) return granted;
|
|
8195
|
+
return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
|
|
8196
|
+
}
|
|
8197
|
+
|
|
8090
8198
|
// src/scheduling.ts
|
|
8091
8199
|
var SCHEDULING_DEFAULTS = {
|
|
8092
8200
|
slotMinutes: 45,
|
|
@@ -8268,8 +8376,8 @@ async function loadTiers(db, group) {
|
|
|
8268
8376
|
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
8269
8377
|
}
|
|
8270
8378
|
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
8271
|
-
const
|
|
8272
|
-
return resolveTiers(group,
|
|
8379
|
+
const list2 = rows3.map((row2) => rowToTier(row2, groupId));
|
|
8380
|
+
return resolveTiers(group, list2);
|
|
8273
8381
|
}
|
|
8274
8382
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
8275
8383
|
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
@@ -8635,9 +8743,9 @@ function optionalText(body, key4, max, errors) {
|
|
|
8635
8743
|
errors.push(`${key4} must be a string`);
|
|
8636
8744
|
return void 0;
|
|
8637
8745
|
}
|
|
8638
|
-
const
|
|
8639
|
-
if (
|
|
8640
|
-
return
|
|
8746
|
+
const text9 = clean(value);
|
|
8747
|
+
if (text9.length > max) errors.push(`${key4} must be at most ${max} characters`);
|
|
8748
|
+
return text9 || null;
|
|
8641
8749
|
}
|
|
8642
8750
|
function validateProfilePatch(body, portal) {
|
|
8643
8751
|
const errors = [];
|
|
@@ -8915,10 +9023,248 @@ var PHOTO_SIZE = 400;
|
|
|
8915
9023
|
// src/worker-routes-hub-members.ts
|
|
8916
9024
|
var HUB_MEMBERS_PATH = "/api/admin/hub-members";
|
|
8917
9025
|
|
|
9026
|
+
// src/referral-codes.ts
|
|
9027
|
+
var REFERRAL_CODE_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
9028
|
+
var REFERRAL_CODE_PATTERN = /^[23456789abcdefghjkmnpqrstuvwxyz]{5,8}$/;
|
|
9029
|
+
var LENGTH = 5;
|
|
9030
|
+
function newReferralCode(random = (n) => crypto.getRandomValues(new Uint8Array(n))) {
|
|
9031
|
+
const bytes = random(LENGTH * 2);
|
|
9032
|
+
let code = "";
|
|
9033
|
+
for (const byte of bytes) {
|
|
9034
|
+
if (byte >= 248) continue;
|
|
9035
|
+
code += REFERRAL_CODE_ALPHABET[byte % REFERRAL_CODE_ALPHABET.length];
|
|
9036
|
+
if (code.length === LENGTH) return code;
|
|
9037
|
+
}
|
|
9038
|
+
return newReferralCode(random);
|
|
9039
|
+
}
|
|
9040
|
+
var generationOf = (rows3) => rows3.reduce((max, row2) => Math.max(max, Number(String(row2.id).split(":").pop()) || 0), 0);
|
|
9041
|
+
async function ensureReferralCode(db, memberId, now = Date.now()) {
|
|
9042
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
9043
|
+
const rows3 = (await db.query({ referralCodes: { $: { where: { memberId }, limit: 100 } } })).referralCodes ?? [];
|
|
9044
|
+
const live = rows3.find((row3) => row3.status === "active");
|
|
9045
|
+
if (live) return live;
|
|
9046
|
+
const id12 = `ref:${memberId}:${generationOf(rows3) + 1}`;
|
|
9047
|
+
const row2 = { id: id12, code: newReferralCode(), memberId, status: "active", createdAt: now };
|
|
9048
|
+
try {
|
|
9049
|
+
await db.transact([{ t: "update", ns: "referralCodes", id: id12, attrs: row2 }], { guards: [{ ns: "referralCodes", id: id12, exists: false }] });
|
|
9050
|
+
return row2;
|
|
9051
|
+
} catch (error) {
|
|
9052
|
+
const code = error.code;
|
|
9053
|
+
if (code !== "transact_guard_failed" && code !== "unique_violation") throw error;
|
|
9054
|
+
}
|
|
9055
|
+
}
|
|
9056
|
+
throw new Error("referral code could not be issued");
|
|
9057
|
+
}
|
|
9058
|
+
async function reissueReferralCode(db, memberId, actor, now = Date.now()) {
|
|
9059
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
9060
|
+
const rows3 = (await db.query({ referralCodes: { $: { where: { memberId }, limit: 100 } } })).referralCodes ?? [];
|
|
9061
|
+
const live = rows3.find((row2) => row2.status === "active");
|
|
9062
|
+
const id12 = `ref:${memberId}:${generationOf(rows3) + 1}`;
|
|
9063
|
+
const next = { id: id12, code: newReferralCode(), memberId, status: "active", createdAt: now };
|
|
9064
|
+
const ops = [{ t: "update", ns: "referralCodes", id: id12, attrs: next }];
|
|
9065
|
+
if (live) ops.unshift({ t: "update", ns: "referralCodes", id: String(live.id), attrs: { status: "deactivated", deactivatedAt: now, deactivatedBy: actor, replacedBy: id12 } });
|
|
9066
|
+
try {
|
|
9067
|
+
await db.transact(ops, { guards: [
|
|
9068
|
+
{ ns: "referralCodes", id: id12, exists: false },
|
|
9069
|
+
...live ? [{ ns: "referralCodes", id: String(live.id), exists: true, equals: { status: "active" } }] : []
|
|
9070
|
+
] });
|
|
9071
|
+
return next;
|
|
9072
|
+
} catch (error) {
|
|
9073
|
+
const code = error.code;
|
|
9074
|
+
if (code !== "transact_guard_failed" && code !== "unique_violation") throw error;
|
|
9075
|
+
}
|
|
9076
|
+
}
|
|
9077
|
+
throw new Error("referral code could not be reissued");
|
|
9078
|
+
}
|
|
9079
|
+
|
|
9080
|
+
// src/referral-events.ts
|
|
9081
|
+
var REFERRAL_COOKIE = "odla_ref";
|
|
9082
|
+
function referralCodeFromCookie(header) {
|
|
9083
|
+
for (const part of (header ?? "").split(";")) {
|
|
9084
|
+
const [name, ...rest] = part.trim().split("=");
|
|
9085
|
+
if (name === REFERRAL_COOKIE) {
|
|
9086
|
+
const value = decodeURIComponent(rest.join("="));
|
|
9087
|
+
return REFERRAL_CODE_PATTERN.test(value) ? value : null;
|
|
9088
|
+
}
|
|
9089
|
+
}
|
|
9090
|
+
return null;
|
|
9091
|
+
}
|
|
9092
|
+
|
|
9093
|
+
// src/referral-ledger.ts
|
|
9094
|
+
function foldLedger(rows3) {
|
|
9095
|
+
const sum = (status2) => rows3.filter((row2) => row2.status === status2).reduce((total, row2) => total + Number(row2.valueCents ?? 0), 0);
|
|
9096
|
+
const earnedCents = sum("earned"), appliedCents = sum("applied"), voidedCents = sum("void");
|
|
9097
|
+
return { earnedCents, appliedCents, voidedCents, carriedForwardCents: sum("carried_forward"), balanceCents: earnedCents - appliedCents - voidedCents };
|
|
9098
|
+
}
|
|
9099
|
+
|
|
9100
|
+
// src/worker-routes-member-referrals.ts
|
|
9101
|
+
var MEMBER_REFERRALS_PATH = "/api/member/referrals";
|
|
9102
|
+
|
|
9103
|
+
// src/worker-routes-referral-link.ts
|
|
9104
|
+
function membershipRedirect(membershipUrl2, origin, code) {
|
|
9105
|
+
const url = new URL(membershipUrl2, origin);
|
|
9106
|
+
if (code) url.searchParams.set("ref", code);
|
|
9107
|
+
return url.toString();
|
|
9108
|
+
}
|
|
9109
|
+
|
|
9110
|
+
// src/course-validate.ts
|
|
9111
|
+
var ID3 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9112
|
+
var KINDS2 = /* @__PURE__ */ new Set(["read", "do", "watch", "extra"]);
|
|
9113
|
+
var rec = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
9114
|
+
function text6(v, path, errors, max, required = false) {
|
|
9115
|
+
if (v === void 0 || v === null || v === "") {
|
|
9116
|
+
if (required) errors.push(`${path} is required`);
|
|
9117
|
+
return void 0;
|
|
9118
|
+
}
|
|
9119
|
+
if (typeof v !== "string") {
|
|
9120
|
+
errors.push(`${path} must be a string`);
|
|
9121
|
+
return void 0;
|
|
9122
|
+
}
|
|
9123
|
+
const t = v.trim();
|
|
9124
|
+
if (t.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
9125
|
+
return t || void 0;
|
|
9126
|
+
}
|
|
9127
|
+
function https(v, path, errors) {
|
|
9128
|
+
const t = text6(v, path, errors, 2e3);
|
|
9129
|
+
if (!t) return void 0;
|
|
9130
|
+
try {
|
|
9131
|
+
if (new URL(t).protocol === "https:") return t;
|
|
9132
|
+
} catch {
|
|
9133
|
+
}
|
|
9134
|
+
errors.push(`${path} must be an https URL`);
|
|
9135
|
+
return void 0;
|
|
9136
|
+
}
|
|
9137
|
+
function list(v, path, errors, max, item) {
|
|
9138
|
+
if (v === void 0) return [];
|
|
9139
|
+
if (!Array.isArray(v)) {
|
|
9140
|
+
errors.push(`${path} must be an array`);
|
|
9141
|
+
return [];
|
|
9142
|
+
}
|
|
9143
|
+
if (v.length > max) errors.push(`${path} supports at most ${max} entries`);
|
|
9144
|
+
return v.slice(0, max).flatMap((raw2, i) => {
|
|
9145
|
+
const r = rec(raw2);
|
|
9146
|
+
if (!r) {
|
|
9147
|
+
errors.push(`${path}[${i}] must be an object`);
|
|
9148
|
+
return [];
|
|
9149
|
+
}
|
|
9150
|
+
const out = item(r, `${path}[${i}]`);
|
|
9151
|
+
return out === void 0 ? [] : [out];
|
|
9152
|
+
});
|
|
9153
|
+
}
|
|
9154
|
+
var material = (errors, withKind) => (r, at2) => {
|
|
9155
|
+
if (withKind && !KINDS2.has(String(r.kind))) errors.push(`${at2}.kind must be read, do, watch or extra`);
|
|
9156
|
+
const label = text6(r.label, `${at2}.label`, errors, 200, true) ?? "";
|
|
9157
|
+
const meta = text6(r.meta, `${at2}.meta`, errors, 200), url = https(r.url, `${at2}.url`, errors);
|
|
9158
|
+
return { ...withKind ? { kind: r.kind } : {}, label, ...meta ? { meta } : {}, ...url ? { url } : {} };
|
|
9159
|
+
};
|
|
9160
|
+
function validateCourseSettings(input, enrolledCount = 0) {
|
|
9161
|
+
const errors = [];
|
|
9162
|
+
const r = rec(input);
|
|
9163
|
+
if (!r) return { ok: false, errors: ["course must be an object"] };
|
|
9164
|
+
const id12 = text6(r.id, "id", errors, 80, true) ?? "";
|
|
9165
|
+
if (id12 && !ID3.test(id12)) errors.push("id must be a lowercase slug");
|
|
9166
|
+
const title = text6(r.title, "title", errors, 200, true) ?? "";
|
|
9167
|
+
const timezone = text6(r.timezone, "timezone", errors, 64, true) ?? "";
|
|
9168
|
+
if (timezone) {
|
|
9169
|
+
try {
|
|
9170
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
9171
|
+
} catch {
|
|
9172
|
+
errors.push("timezone must be an IANA time zone");
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
9175
|
+
const capacity = Number(r.capacity);
|
|
9176
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 100) errors.push("capacity must be an integer from 1 to 100");
|
|
9177
|
+
else if (capacity < enrolledCount) errors.push(`capacity cannot be below the ${enrolledCount} seats already taken`);
|
|
9178
|
+
const signupClosesAt = Number(r.signupClosesAt);
|
|
9179
|
+
if (!Number.isSafeInteger(signupClosesAt) || signupClosesAt <= 0) errors.push("signupClosesAt must be a timestamp");
|
|
9180
|
+
const c = rec(r.content ?? {}) ?? {};
|
|
9181
|
+
const outcomes = Array.isArray(c.outcomes) ? c.outcomes.slice(0, 12).map((o, i) => text6(o, `content.outcomes[${i}]`, errors, 300, true) ?? "") : [];
|
|
9182
|
+
const content = {};
|
|
9183
|
+
for (const key4 of ["basedOn", "short", "when", "start", "dates", "time"]) {
|
|
9184
|
+
const v = text6(c[key4], `content.${key4}`, errors, 600);
|
|
9185
|
+
if (v) content[key4] = v;
|
|
9186
|
+
}
|
|
9187
|
+
const partner = text6(c.partnerMarkdown, "content.partnerMarkdown", errors, 4e3);
|
|
9188
|
+
if (partner) content.partnerMarkdown = partner;
|
|
9189
|
+
if (outcomes.length) content.outcomes = outcomes;
|
|
9190
|
+
const sessions = list(r.sessions, "sessions", errors, 26, (s, at2) => {
|
|
9191
|
+
const startsAt = Number(s.startsAt), endsAt = Number(s.endsAt);
|
|
9192
|
+
if (!Number.isSafeInteger(startsAt) || !Number.isSafeInteger(endsAt) || endsAt <= startsAt) errors.push(`${at2} needs startsAt before endsAt`);
|
|
9193
|
+
const date = text6(s.date, `${at2}.date`, errors, 40, true) ?? "";
|
|
9194
|
+
const topic = text6(s.topic, `${at2}.topic`, errors, 200, true) ?? "";
|
|
9195
|
+
const description = text6(s.description, `${at2}.description`, errors, 1e3);
|
|
9196
|
+
return { date, startsAt, endsAt, topic, ...description ? { description } : {}, materials: list(s.materials, `${at2}.materials`, errors, 20, material(errors, true)) };
|
|
9197
|
+
});
|
|
9198
|
+
if (!sessions.length) errors.push("sessions must list at least one session");
|
|
9199
|
+
for (let i = 1; i < sessions.length; i += 1) if (sessions[i].startsAt <= sessions[i - 1].startsAt) errors.push(`sessions[${i}] must start after sessions[${i - 1}]`);
|
|
9200
|
+
const toolkit = list(r.toolkit, "toolkit", errors, 20, material(errors, false));
|
|
9201
|
+
const guides = list(r.guides, "guides", errors, 8, (g, at2) => {
|
|
9202
|
+
const name = text6(g.name, `${at2}.name`, errors, 120, true) ?? "";
|
|
9203
|
+
const role = text6(g.role, `${at2}.role`, errors, 200), bio = text6(g.bio, `${at2}.bio`, errors, 1500), photoUrl = https(g.photoUrl, `${at2}.photoUrl`, errors);
|
|
9204
|
+
return { name, ...role ? { role } : {}, ...bio ? { bio } : {}, ...photoUrl ? { photoUrl } : {} };
|
|
9205
|
+
});
|
|
9206
|
+
if (errors.length) return { ok: false, errors };
|
|
9207
|
+
return { ok: true, value: { id: id12, title, timezone, capacity, signupClosesAt, content, sessions, toolkit, guides } };
|
|
9208
|
+
}
|
|
9209
|
+
|
|
9210
|
+
// src/course-calendar.ts
|
|
9211
|
+
var import_calendar = require("@odla-ai/calendar");
|
|
9212
|
+
function courseEventInput(course) {
|
|
9213
|
+
const first = course.sessions[0];
|
|
9214
|
+
return {
|
|
9215
|
+
summary: course.title,
|
|
9216
|
+
startAt: first.startsAt,
|
|
9217
|
+
endAt: first.endsAt,
|
|
9218
|
+
timezone: course.timezone,
|
|
9219
|
+
attendees: [],
|
|
9220
|
+
meet: true,
|
|
9221
|
+
recurrence: [`RRULE:FREQ=WEEKLY;COUNT=${course.sessions.length}`],
|
|
9222
|
+
guestsCanSeeOtherGuests: false,
|
|
9223
|
+
guestsCanInviteOthers: false,
|
|
9224
|
+
guestsCanModify: false,
|
|
9225
|
+
skipAvailabilityCheck: true
|
|
9226
|
+
};
|
|
9227
|
+
}
|
|
9228
|
+
|
|
9229
|
+
// src/course-store.ts
|
|
9230
|
+
var waitlistPosition = (enrollments, memberId) => {
|
|
9231
|
+
const index2 = enrollments.filter((row2) => row2.status === "waitlisted").findIndex((row2) => row2.memberId === memberId);
|
|
9232
|
+
return index2 < 0 ? null : index2 + 1;
|
|
9233
|
+
};
|
|
9234
|
+
|
|
9235
|
+
// src/worker-routes-member-course.ts
|
|
9236
|
+
function memberCourseView(course, enrollments, memberId, now = Date.now()) {
|
|
9237
|
+
const mine = enrollments.find((row2) => row2.memberId === memberId && ["enrolled", "waitlisted", "pending"].includes(String(row2.status)));
|
|
9238
|
+
const enrolled = mine?.status === "enrolled";
|
|
9239
|
+
const seatsLeft = Math.max(0, Number(course.capacity) - Number(course.enrolledCount));
|
|
9240
|
+
const visible = (material2) => enrolled ? material2 : (({ url: _url, ...rest }) => rest)(material2);
|
|
9241
|
+
const sessions = (course.sessions ?? []).map((session) => ({
|
|
9242
|
+
...session,
|
|
9243
|
+
materials: session.materials.filter((material2) => material2.url).map(visible)
|
|
9244
|
+
}));
|
|
9245
|
+
return {
|
|
9246
|
+
course: {
|
|
9247
|
+
id: course.id,
|
|
9248
|
+
title: course.title,
|
|
9249
|
+
timezone: course.timezone,
|
|
9250
|
+
content: course.content ?? {},
|
|
9251
|
+
guides: course.guides ?? [],
|
|
9252
|
+
capacity: Number(course.capacity),
|
|
9253
|
+
seatsLeft,
|
|
9254
|
+
signupClosesAt: course.signupClosesAt,
|
|
9255
|
+
sessions,
|
|
9256
|
+
toolkit: (course.toolkit ?? []).filter((item) => item.url).map(visible),
|
|
9257
|
+
status: now >= Number(course.signupClosesAt) ? "closed" : seatsLeft === 0 ? "full" : "open"
|
|
9258
|
+
},
|
|
9259
|
+
enrollment: mine ? { status: mine.status, waitlistPosition: waitlistPosition(enrollments, memberId) } : null,
|
|
9260
|
+
...enrolled && course.meetUrl ? { meetUrl: course.meetUrl } : {}
|
|
9261
|
+
};
|
|
9262
|
+
}
|
|
9263
|
+
|
|
8918
9264
|
// src/email-retirement.ts
|
|
8919
|
-
function isRetiredEmailCopy(subject,
|
|
9265
|
+
function isRetiredEmailCopy(subject, text9) {
|
|
8920
9266
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
8921
|
-
const bodyCopy =
|
|
9267
|
+
const bodyCopy = text9.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
8922
9268
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
8923
9269
|
}
|
|
8924
9270
|
|
|
@@ -8943,8 +9289,8 @@ function lineHtml(line) {
|
|
|
8943
9289
|
}
|
|
8944
9290
|
return out + escapeHtml(line.slice(last));
|
|
8945
9291
|
}
|
|
8946
|
-
function textToHtml(
|
|
8947
|
-
const paragraphs =
|
|
9292
|
+
function textToHtml(text9) {
|
|
9293
|
+
const paragraphs = text9.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
8948
9294
|
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
8949
9295
|
return `<!doctype html>
|
|
8950
9296
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
@@ -9049,12 +9395,12 @@ function planDelivery(input) {
|
|
|
9049
9395
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
9050
9396
|
if ((0, import_crm7.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
9051
9397
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
9052
|
-
const
|
|
9398
|
+
const text9 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
9053
9399
|
|
|
9054
9400
|
` + renderedText : renderedText;
|
|
9055
9401
|
const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
|
|
9056
9402
|
const htmlOut = renderedHtml && redirect ? `<p style="font:13px monospace;color:#555;margin:0 0 12px">(dev redirect; original recipient: ${escapeHtml(input.to)})</p>` + renderedHtml : renderedHtml;
|
|
9057
|
-
return { deliver: true, transport, to, subject, text:
|
|
9403
|
+
return { deliver: true, transport, to, subject, text: text9, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
|
|
9058
9404
|
}
|
|
9059
9405
|
|
|
9060
9406
|
// src/email-readiness.ts
|
|
@@ -9094,7 +9440,7 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
|
9094
9440
|
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
9095
9441
|
function emailSpamSignals(copy) {
|
|
9096
9442
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
9097
|
-
const
|
|
9443
|
+
const text9 = typeof copy.text === "string" ? copy.text : "";
|
|
9098
9444
|
const html = typeof copy.html === "string" ? copy.html : "";
|
|
9099
9445
|
const out = [];
|
|
9100
9446
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
@@ -9104,11 +9450,11 @@ function emailSpamSignals(copy) {
|
|
|
9104
9450
|
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
9105
9451
|
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
9106
9452
|
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
9107
|
-
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(
|
|
9453
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text9));
|
|
9108
9454
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
9109
|
-
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(
|
|
9110
|
-
if (/!{2,}/.test(
|
|
9111
|
-
const urls = [...`${
|
|
9455
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text9))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
9456
|
+
if (/!{2,}/.test(text9)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
9457
|
+
const urls = [...`${text9}
|
|
9112
9458
|
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
9113
9459
|
const hosts = urls.map((url) => {
|
|
9114
9460
|
try {
|
|
@@ -9121,7 +9467,7 @@ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
|
9121
9467
|
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
9122
9468
|
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
9123
9469
|
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
9124
|
-
if (urls.length && words(
|
|
9470
|
+
if (urls.length && words(text9).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
9125
9471
|
return out;
|
|
9126
9472
|
}
|
|
9127
9473
|
function emailSenderWarnings(input) {
|
|
@@ -9325,14 +9671,14 @@ async function sendTemplated(deps, input) {
|
|
|
9325
9671
|
if (decision.transport === "log-only") return { sent: true };
|
|
9326
9672
|
let messageId;
|
|
9327
9673
|
try {
|
|
9328
|
-
const
|
|
9329
|
-
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(
|
|
9674
|
+
const text9 = String(row2.body);
|
|
9675
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text9);
|
|
9330
9676
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
9331
9677
|
const response = await deps.sender.send({
|
|
9332
9678
|
from: fromAddress(deps.from, input.group.fromName),
|
|
9333
9679
|
to: [decision.to],
|
|
9334
9680
|
subject: String(row2.subject),
|
|
9335
|
-
text:
|
|
9681
|
+
text: text9,
|
|
9336
9682
|
...html ? { html } : {},
|
|
9337
9683
|
...headers ? { headers } : {},
|
|
9338
9684
|
replyTo: input.group.replyTo
|
|
@@ -9584,7 +9930,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
|
|
|
9584
9930
|
}
|
|
9585
9931
|
|
|
9586
9932
|
// src/admission-grants.ts
|
|
9587
|
-
var
|
|
9933
|
+
var ID4 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
9588
9934
|
var MAX_GRANTS = 2e3;
|
|
9589
9935
|
var MAX_NOTE = 2e3;
|
|
9590
9936
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -9606,10 +9952,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
9606
9952
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
9607
9953
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
9608
9954
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
9609
|
-
if (!
|
|
9955
|
+
if (!ID4.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
9610
9956
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
9611
|
-
if (!
|
|
9612
|
-
if (!
|
|
9957
|
+
if (!ID4.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
9958
|
+
if (!ID4.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
9613
9959
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
9614
9960
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
9615
9961
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -9624,7 +9970,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
9624
9970
|
const decision = grant.decision;
|
|
9625
9971
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
9626
9972
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
9627
|
-
if (!
|
|
9973
|
+
if (!ID4.test(id12)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
9628
9974
|
if (seenIds.has(id12)) errors.push(`document.grants repeats id "${id12}"`);
|
|
9629
9975
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
9630
9976
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -9783,11 +10129,11 @@ function resolveJoinContinuation(input) {
|
|
|
9783
10129
|
// src/gift-offers.ts
|
|
9784
10130
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
9785
10131
|
var MAX_GIFT_OFFERS = 20;
|
|
9786
|
-
var
|
|
10132
|
+
var ID5 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
9787
10133
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
9788
10134
|
var CURRENCY = /^[a-z]{3}$/;
|
|
9789
10135
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
9790
|
-
var
|
|
10136
|
+
var text7 = (value, path, errors, max) => {
|
|
9791
10137
|
if (typeof value !== "string" || !value.trim()) {
|
|
9792
10138
|
errors.push(`${path} is required`);
|
|
9793
10139
|
return "";
|
|
@@ -9815,23 +10161,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
9815
10161
|
}
|
|
9816
10162
|
const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
9817
10163
|
for (const key4 of Object.keys(item)) if (!allowed.has(key4)) errors.push(`${path}.${key4} is not supported`);
|
|
9818
|
-
const id12 =
|
|
9819
|
-
if (id12 && !
|
|
10164
|
+
const id12 = text7(item.id, `${path}.id`, errors, 80);
|
|
10165
|
+
if (id12 && !ID5.test(id12)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9820
10166
|
if (ids2.has(id12)) errors.push(`${path}.id duplicates "${id12}"`);
|
|
9821
10167
|
ids2.add(id12);
|
|
9822
10168
|
const targetRaw = record3(item.target);
|
|
9823
10169
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
9824
10170
|
if (targetRaw?.type === "tier") {
|
|
9825
|
-
const tierId =
|
|
10171
|
+
const tierId = text7(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
9826
10172
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
9827
10173
|
target = { type: "tier", tierId };
|
|
9828
10174
|
} else if (targetRaw?.type === "entitlement") {
|
|
9829
|
-
const entitlementKey =
|
|
9830
|
-
if (entitlementKey && !
|
|
10175
|
+
const entitlementKey = text7(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
10176
|
+
if (entitlementKey && !ID5.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
9831
10177
|
target = { type: "entitlement", entitlementKey };
|
|
9832
10178
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
9833
10179
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
9834
|
-
const stripePriceId =
|
|
10180
|
+
const stripePriceId = text7(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
9835
10181
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
9836
10182
|
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`);
|
|
9837
10183
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -9853,9 +10199,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
9853
10199
|
redemptionLifetimeHours: lifetime,
|
|
9854
10200
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
9855
10201
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
9856
|
-
title:
|
|
9857
|
-
description:
|
|
9858
|
-
ctaLabel:
|
|
10202
|
+
title: text7(item.title, `${path}.title`, errors, 160),
|
|
10203
|
+
description: text7(item.description, `${path}.description`, errors, 2e3),
|
|
10204
|
+
ctaLabel: text7(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
9859
10205
|
});
|
|
9860
10206
|
}
|
|
9861
10207
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -10633,10 +10979,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
10633
10979
|
errors.push(`${path} must be a string`);
|
|
10634
10980
|
return void 0;
|
|
10635
10981
|
}
|
|
10636
|
-
const
|
|
10637
|
-
if (required && !
|
|
10638
|
-
if (
|
|
10639
|
-
return
|
|
10982
|
+
const text9 = value.trim();
|
|
10983
|
+
if (required && !text9) errors.push(`${path} is required`);
|
|
10984
|
+
if (text9.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
10985
|
+
return text9;
|
|
10640
10986
|
}
|
|
10641
10987
|
function validateMembershipPageDocument(input, options = {}) {
|
|
10642
10988
|
const errors = [];
|
|
@@ -10685,8 +11031,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
10685
11031
|
else {
|
|
10686
11032
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
10687
11033
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
10688
|
-
const
|
|
10689
|
-
return
|
|
11034
|
+
const text9 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
11035
|
+
return text9 ? [text9] : [];
|
|
10690
11036
|
});
|
|
10691
11037
|
}
|
|
10692
11038
|
}
|
|
@@ -10785,7 +11131,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
10785
11131
|
function unknownKeys3(value, allowed, path, errors) {
|
|
10786
11132
|
for (const key4 of Object.keys(value)) if (!allowed.has(key4)) errors.push(`${path}.${key4} is not supported`);
|
|
10787
11133
|
}
|
|
10788
|
-
function
|
|
11134
|
+
function text8(value, path, errors, max = 65536) {
|
|
10789
11135
|
if (typeof value !== "string" || !value.trim()) {
|
|
10790
11136
|
errors.push(`${path} is required`);
|
|
10791
11137
|
return "";
|
|
@@ -10808,11 +11154,11 @@ function tiersOf(value, errors) {
|
|
|
10808
11154
|
return [];
|
|
10809
11155
|
}
|
|
10810
11156
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
10811
|
-
const id12 =
|
|
11157
|
+
const id12 = text8(item.id, `${path}.id`, errors, 80);
|
|
10812
11158
|
if (id12 && !TIER.test(id12)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
10813
11159
|
if (seen.has(id12)) errors.push(`${path}.id duplicates "${id12}"`);
|
|
10814
11160
|
seen.add(id12);
|
|
10815
|
-
const name =
|
|
11161
|
+
const name = text8(item.name, `${path}.name`, errors, 160);
|
|
10816
11162
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
10817
11163
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
10818
11164
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -10830,7 +11176,7 @@ function tiersOf(value, errors) {
|
|
|
10830
11176
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
10831
11177
|
errors.push(`${path}.interval must be year or month`);
|
|
10832
11178
|
}
|
|
10833
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
11179
|
+
const blurb = item.blurb === void 0 ? void 0 : text8(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
10834
11180
|
return [{
|
|
10835
11181
|
id: id12,
|
|
10836
11182
|
name,
|
|
@@ -10888,7 +11234,7 @@ function policyOf(value, errors, tiers2) {
|
|
|
10888
11234
|
const result = {};
|
|
10889
11235
|
for (const key4 of allowed) {
|
|
10890
11236
|
if (input[key4] === void 0 && key4 !== "disclaimerText" && key4 !== "refundPolicyText") continue;
|
|
10891
|
-
result[key4] =
|
|
11237
|
+
result[key4] = text8(input[key4], `document.policy.${key4}`, errors);
|
|
10892
11238
|
}
|
|
10893
11239
|
const tierPolicies = {};
|
|
10894
11240
|
if (input.tierPolicies !== void 0) {
|
|
@@ -10903,8 +11249,8 @@ function policyOf(value, errors, tiers2) {
|
|
|
10903
11249
|
}
|
|
10904
11250
|
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
10905
11251
|
tierPolicies[id12] = {
|
|
10906
|
-
refundPolicyText:
|
|
10907
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
11252
|
+
refundPolicyText: text8(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
11253
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text8(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
10908
11254
|
};
|
|
10909
11255
|
}
|
|
10910
11256
|
}
|
|
@@ -10944,9 +11290,9 @@ function validateSignupControlDocument(input) {
|
|
|
10944
11290
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
10945
11291
|
const source = recordOf(root.source);
|
|
10946
11292
|
const target = recordOf(root.target);
|
|
10947
|
-
const appId =
|
|
10948
|
-
const chapterId =
|
|
10949
|
-
const environment =
|
|
11293
|
+
const appId = text8(source?.appId, "document.source.appId", errors, 63);
|
|
11294
|
+
const chapterId = text8(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
11295
|
+
const environment = text8(target?.environment, "document.target.environment", errors, 63);
|
|
10950
11296
|
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
10951
11297
|
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
10952
11298
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -11123,9 +11469,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
11123
11469
|
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
11124
11470
|
continue;
|
|
11125
11471
|
}
|
|
11126
|
-
const
|
|
11127
|
-
if (
|
|
11128
|
-
if (
|
|
11472
|
+
const text9 = (match[1] ?? "").trim();
|
|
11473
|
+
if (text9.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
11474
|
+
if (text9) benefits.push(text9);
|
|
11129
11475
|
}
|
|
11130
11476
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
11131
11477
|
return benefits.slice(0, MAX_BENEFITS2);
|