@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.js
CHANGED
|
@@ -281,7 +281,10 @@ var applicationAttrs = {
|
|
|
281
281
|
namedSeatAcceptedAt: attr("number", { optional: true }),
|
|
282
282
|
approvedAt: attr("number", { optional: true }),
|
|
283
283
|
entitlementTierId: attr("string", { optional: true, indexed: true }),
|
|
284
|
-
entitlementTierEndsAt: attr("number", { optional: true })
|
|
284
|
+
entitlementTierEndsAt: attr("number", { optional: true }),
|
|
285
|
+
referredByMemberId: attr("string", { optional: true, indexed: true }),
|
|
286
|
+
referralCode: attr("string", { optional: true }),
|
|
287
|
+
referredAt: attr("number", { optional: true })
|
|
285
288
|
};
|
|
286
289
|
function applicationsFor(application) {
|
|
287
290
|
const attrs = {};
|
|
@@ -1723,6 +1726,89 @@ var DEFAULT_PORTAL_COPY = {
|
|
|
1723
1726
|
saved: "Saved",
|
|
1724
1727
|
conflict: "Your profile changed in another window. Reload to see the latest version.",
|
|
1725
1728
|
saveFailed: "Your profile could not be saved. Please try again."
|
|
1729
|
+
},
|
|
1730
|
+
share: {
|
|
1731
|
+
eyebrow: "Community",
|
|
1732
|
+
intro: "Invite the people you would like to have beside you.",
|
|
1733
|
+
officialTitle: "Make it official on LinkedIn",
|
|
1734
|
+
officialBody: "Add your membership to your LinkedIn profile. It takes two minutes.",
|
|
1735
|
+
showMeHow: "Show me how",
|
|
1736
|
+
stepsEyebrow: "Two minutes, three steps",
|
|
1737
|
+
steps: [
|
|
1738
|
+
"On your profile, select Add profile section, then Add position.",
|
|
1739
|
+
"Title: {tierName}. Company: choose us from the list.",
|
|
1740
|
+
"Paste the description into the description box and save."
|
|
1741
|
+
],
|
|
1742
|
+
openLinkedIn: "Open my LinkedIn profile",
|
|
1743
|
+
copyDescription: "Copy description",
|
|
1744
|
+
linkedinDescription: "{tierName}.",
|
|
1745
|
+
postTitle: "Post about us",
|
|
1746
|
+
postBody: "Tell your network you are here. The post is already written.",
|
|
1747
|
+
shareOnLinkedIn: "Share on LinkedIn",
|
|
1748
|
+
previewPost: "Preview the post",
|
|
1749
|
+
linkedinPost: "I'm proud to be {article} {tierName}.",
|
|
1750
|
+
linkTitle: "Your personal invite link",
|
|
1751
|
+
linkBody: "Send it to anyone you would like beside you.",
|
|
1752
|
+
linkCredit: "When a friend joins as a paying member with your link, {credit} comes off your next year's membership.",
|
|
1753
|
+
referralsCredit: "Each friend who joins as a paying member takes {credit} off your next year's membership.",
|
|
1754
|
+
copyLink: "Copy link",
|
|
1755
|
+
copied: "Copied!",
|
|
1756
|
+
copyBlocked: "Copy not allowed here",
|
|
1757
|
+
emailInvite: "Email an invitation",
|
|
1758
|
+
textInvite: "Text an invitation",
|
|
1759
|
+
inviteNote: "Each opens your email or messages with a note already written. You choose who it goes to.",
|
|
1760
|
+
emailSubject: "Join me",
|
|
1761
|
+
emailBody: "Hi,\n\nI thought of you. Here's my personal invite link:\n{inviteUrl}\n\n{firstName}",
|
|
1762
|
+
textBody: "Here's my invite link if you're curious: {inviteUrl}",
|
|
1763
|
+
fallbackEmail: "Your email is ready. If your mail app didn't open, copy the message and paste it into an email.",
|
|
1764
|
+
fallbackText: "Your text is ready. If your messages app didn't open, copy the message and paste it into a text.",
|
|
1765
|
+
copyMessage: "Copy message",
|
|
1766
|
+
close: "Close",
|
|
1767
|
+
referralsTitle: "Your referrals",
|
|
1768
|
+
referralsBody: "People who joined with your personal invite link.",
|
|
1769
|
+
inviteFriend: "Invite a friend",
|
|
1770
|
+
joined: "Joined",
|
|
1771
|
+
inProcess: "In process",
|
|
1772
|
+
offNextYear: "Off next year",
|
|
1773
|
+
joinedWith: "Joined with your link: {names}"
|
|
1774
|
+
},
|
|
1775
|
+
course: {
|
|
1776
|
+
eyebrow: "Live course \xB7 Registration open",
|
|
1777
|
+
reserve: "Reserve my seat",
|
|
1778
|
+
seeDetails: "See details",
|
|
1779
|
+
courseDetails: "Course details",
|
|
1780
|
+
youreIn: "You're in",
|
|
1781
|
+
invitesOnWay: "Calendar invitations for every session are on their way, each with the Google Meet link.",
|
|
1782
|
+
openMeet: "Open Google Meet",
|
|
1783
|
+
giveUp: "Give up my seat",
|
|
1784
|
+
confirmGiveUp: "Give up your seat? Your calendar invitations will be canceled.",
|
|
1785
|
+
joinWaitlist: "Join the waitlist",
|
|
1786
|
+
onWaitlist: "On the waitlist, number {position}",
|
|
1787
|
+
leaveWaitlist: "Leave the waitlist",
|
|
1788
|
+
seatsOpen: "{left} of {capacity} seats open",
|
|
1789
|
+
full: "The course is full",
|
|
1790
|
+
closed: "Sign-up has closed",
|
|
1791
|
+
when: "When",
|
|
1792
|
+
dates: "Dates",
|
|
1793
|
+
where: "Where",
|
|
1794
|
+
seats: "Seats",
|
|
1795
|
+
googleMeet: "Google Meet",
|
|
1796
|
+
inviteEvery: "We'll send a calendar invitation for every session.",
|
|
1797
|
+
guidesTitle: "Your guides",
|
|
1798
|
+
sessionsTitle: "Sessions",
|
|
1799
|
+
materialsOpen: "Open a session for what to watch, read, and do beforehand.",
|
|
1800
|
+
materialsLocked: "Materials open when you reserve your seat.",
|
|
1801
|
+
items: "{count} items",
|
|
1802
|
+
outcomesTitle: "You'll leave with",
|
|
1803
|
+
toolkitTitle: "Your toolkit",
|
|
1804
|
+
timeTitle: "Time",
|
|
1805
|
+
open: "Open",
|
|
1806
|
+
read: "Read",
|
|
1807
|
+
do: "Do",
|
|
1808
|
+
watch: "Watch",
|
|
1809
|
+
extra: "Optional",
|
|
1810
|
+
failed: "Something went wrong. Please try again.",
|
|
1811
|
+
reserveYourSeat: "Reserve your seat"
|
|
1726
1812
|
}
|
|
1727
1813
|
};
|
|
1728
1814
|
|
|
@@ -1782,8 +1868,8 @@ var APPLICATION_LABELS = {
|
|
|
1782
1868
|
state: "State"
|
|
1783
1869
|
};
|
|
1784
1870
|
function humanize(id12) {
|
|
1785
|
-
const
|
|
1786
|
-
return
|
|
1871
|
+
const text9 = id12.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1872
|
+
return text9 ? `${text9[0].toUpperCase()}${text9.slice(1)}` : id12;
|
|
1787
1873
|
}
|
|
1788
1874
|
function applicationDefs(chapter) {
|
|
1789
1875
|
try {
|
|
@@ -4378,24 +4464,24 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
4378
4464
|
let earliest;
|
|
4379
4465
|
const seen = /* @__PURE__ */ new Set();
|
|
4380
4466
|
for (let page = 0; page < 10; page++) {
|
|
4381
|
-
const
|
|
4467
|
+
const list2 = await call("GET", "/v1/invoices", {
|
|
4382
4468
|
subscription: input.subscriptionId,
|
|
4383
4469
|
customer: input.customerId,
|
|
4384
4470
|
status: "paid",
|
|
4385
4471
|
limit: 100,
|
|
4386
4472
|
...cursor ? { starting_after: cursor } : {}
|
|
4387
4473
|
});
|
|
4388
|
-
if (!Array.isArray(
|
|
4389
|
-
for (const value of
|
|
4474
|
+
if (!Array.isArray(list2.data) || typeof list2.has_more !== "boolean") fail3("stripe_invalid_response");
|
|
4475
|
+
for (const value of list2.data) {
|
|
4390
4476
|
const invoice2 = row(value);
|
|
4391
4477
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
4392
4478
|
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();
|
|
4393
4479
|
seen.add(id8(invoice2));
|
|
4394
4480
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
4395
4481
|
}
|
|
4396
|
-
if (!
|
|
4397
|
-
if (!
|
|
4398
|
-
cursor = id8(
|
|
4482
|
+
if (!list2.has_more) break;
|
|
4483
|
+
if (!list2.data.length || page === 9) fail3("refund_history_incomplete");
|
|
4484
|
+
cursor = id8(list2.data[list2.data.length - 1]);
|
|
4399
4485
|
}
|
|
4400
4486
|
if (!earliest) fail3("no_charge");
|
|
4401
4487
|
const invoice = earliest;
|
|
@@ -5631,11 +5717,11 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
|
5631
5717
|
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
5632
5718
|
}
|
|
5633
5719
|
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
5634
|
-
const
|
|
5635
|
-
if (!Array.isArray(
|
|
5720
|
+
const list2 = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
5721
|
+
if (!Array.isArray(list2.data) || list2.has_more !== false || list2.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
5636
5722
|
let refundedCents = 0;
|
|
5637
5723
|
const refundIds = [];
|
|
5638
|
-
for (const refund of
|
|
5724
|
+
for (const refund of list2.data) {
|
|
5639
5725
|
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();
|
|
5640
5726
|
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
5641
5727
|
if (refund.status === "succeeded") {
|
|
@@ -5686,11 +5772,11 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
5686
5772
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
5687
5773
|
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
5688
5774
|
}
|
|
5689
|
-
const
|
|
5690
|
-
if (!Array.isArray(
|
|
5775
|
+
const list2 = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
5776
|
+
if (!Array.isArray(list2.data) || list2.has_more !== false || list2.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
5691
5777
|
let refundedCents = 0, pendingCents = 0;
|
|
5692
5778
|
const refundIds = [];
|
|
5693
|
-
for (const refund of
|
|
5779
|
+
for (const refund of list2.data) {
|
|
5694
5780
|
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();
|
|
5695
5781
|
if (refund.status === "succeeded") {
|
|
5696
5782
|
refundedCents += refund.amount;
|
|
@@ -6371,7 +6457,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6371
6457
|
};
|
|
6372
6458
|
signal.addEventListener("abort", cancel, { once: true });
|
|
6373
6459
|
let size = 0;
|
|
6374
|
-
let
|
|
6460
|
+
let text9 = "";
|
|
6375
6461
|
const decoder = new TextDecoder();
|
|
6376
6462
|
try {
|
|
6377
6463
|
while (true) {
|
|
@@ -6381,10 +6467,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6381
6467
|
if (done) break;
|
|
6382
6468
|
size += value.byteLength;
|
|
6383
6469
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
6384
|
-
|
|
6470
|
+
text9 += decoder.decode(value, { stream: true });
|
|
6385
6471
|
}
|
|
6386
6472
|
try {
|
|
6387
|
-
return JSON.parse(
|
|
6473
|
+
return JSON.parse(text9 + decoder.decode());
|
|
6388
6474
|
} catch {
|
|
6389
6475
|
throw new TypeError(`invalid ${label}`);
|
|
6390
6476
|
}
|
|
@@ -7771,6 +7857,14 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
7771
7857
|
return { document, digest: await builderLibraryDigest(document) };
|
|
7772
7858
|
}
|
|
7773
7859
|
|
|
7860
|
+
// src/member-entitlement.ts
|
|
7861
|
+
function entitledTierId(app, now = Date.now()) {
|
|
7862
|
+
const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
|
|
7863
|
+
const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
|
|
7864
|
+
if (granted && (endsAt === null || endsAt > now)) return granted;
|
|
7865
|
+
return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
|
|
7866
|
+
}
|
|
7867
|
+
|
|
7774
7868
|
// src/scheduling.ts
|
|
7775
7869
|
var SCHEDULING_DEFAULTS = {
|
|
7776
7870
|
slotMinutes: 45,
|
|
@@ -7952,8 +8046,8 @@ async function loadTiers(db, group) {
|
|
|
7952
8046
|
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
7953
8047
|
}
|
|
7954
8048
|
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
7955
|
-
const
|
|
7956
|
-
return resolveTiers(group,
|
|
8049
|
+
const list2 = rows3.map((row2) => rowToTier(row2, groupId));
|
|
8050
|
+
return resolveTiers(group, list2);
|
|
7957
8051
|
}
|
|
7958
8052
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
7959
8053
|
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
@@ -8319,9 +8413,9 @@ function optionalText(body, key4, max, errors) {
|
|
|
8319
8413
|
errors.push(`${key4} must be a string`);
|
|
8320
8414
|
return void 0;
|
|
8321
8415
|
}
|
|
8322
|
-
const
|
|
8323
|
-
if (
|
|
8324
|
-
return
|
|
8416
|
+
const text9 = clean(value);
|
|
8417
|
+
if (text9.length > max) errors.push(`${key4} must be at most ${max} characters`);
|
|
8418
|
+
return text9 || null;
|
|
8325
8419
|
}
|
|
8326
8420
|
function validateProfilePatch(body, portal) {
|
|
8327
8421
|
const errors = [];
|
|
@@ -8599,10 +8693,248 @@ var PHOTO_SIZE = 400;
|
|
|
8599
8693
|
// src/worker-routes-hub-members.ts
|
|
8600
8694
|
var HUB_MEMBERS_PATH = "/api/admin/hub-members";
|
|
8601
8695
|
|
|
8696
|
+
// src/referral-codes.ts
|
|
8697
|
+
var REFERRAL_CODE_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
8698
|
+
var REFERRAL_CODE_PATTERN = /^[23456789abcdefghjkmnpqrstuvwxyz]{5,8}$/;
|
|
8699
|
+
var LENGTH = 5;
|
|
8700
|
+
function newReferralCode(random = (n) => crypto.getRandomValues(new Uint8Array(n))) {
|
|
8701
|
+
const bytes = random(LENGTH * 2);
|
|
8702
|
+
let code = "";
|
|
8703
|
+
for (const byte of bytes) {
|
|
8704
|
+
if (byte >= 248) continue;
|
|
8705
|
+
code += REFERRAL_CODE_ALPHABET[byte % REFERRAL_CODE_ALPHABET.length];
|
|
8706
|
+
if (code.length === LENGTH) return code;
|
|
8707
|
+
}
|
|
8708
|
+
return newReferralCode(random);
|
|
8709
|
+
}
|
|
8710
|
+
var generationOf = (rows3) => rows3.reduce((max, row2) => Math.max(max, Number(String(row2.id).split(":").pop()) || 0), 0);
|
|
8711
|
+
async function ensureReferralCode(db, memberId, now = Date.now()) {
|
|
8712
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
8713
|
+
const rows3 = (await db.query({ referralCodes: { $: { where: { memberId }, limit: 100 } } })).referralCodes ?? [];
|
|
8714
|
+
const live = rows3.find((row3) => row3.status === "active");
|
|
8715
|
+
if (live) return live;
|
|
8716
|
+
const id12 = `ref:${memberId}:${generationOf(rows3) + 1}`;
|
|
8717
|
+
const row2 = { id: id12, code: newReferralCode(), memberId, status: "active", createdAt: now };
|
|
8718
|
+
try {
|
|
8719
|
+
await db.transact([{ t: "update", ns: "referralCodes", id: id12, attrs: row2 }], { guards: [{ ns: "referralCodes", id: id12, exists: false }] });
|
|
8720
|
+
return row2;
|
|
8721
|
+
} catch (error) {
|
|
8722
|
+
const code = error.code;
|
|
8723
|
+
if (code !== "transact_guard_failed" && code !== "unique_violation") throw error;
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
throw new Error("referral code could not be issued");
|
|
8727
|
+
}
|
|
8728
|
+
async function reissueReferralCode(db, memberId, actor, now = Date.now()) {
|
|
8729
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
8730
|
+
const rows3 = (await db.query({ referralCodes: { $: { where: { memberId }, limit: 100 } } })).referralCodes ?? [];
|
|
8731
|
+
const live = rows3.find((row2) => row2.status === "active");
|
|
8732
|
+
const id12 = `ref:${memberId}:${generationOf(rows3) + 1}`;
|
|
8733
|
+
const next = { id: id12, code: newReferralCode(), memberId, status: "active", createdAt: now };
|
|
8734
|
+
const ops = [{ t: "update", ns: "referralCodes", id: id12, attrs: next }];
|
|
8735
|
+
if (live) ops.unshift({ t: "update", ns: "referralCodes", id: String(live.id), attrs: { status: "deactivated", deactivatedAt: now, deactivatedBy: actor, replacedBy: id12 } });
|
|
8736
|
+
try {
|
|
8737
|
+
await db.transact(ops, { guards: [
|
|
8738
|
+
{ ns: "referralCodes", id: id12, exists: false },
|
|
8739
|
+
...live ? [{ ns: "referralCodes", id: String(live.id), exists: true, equals: { status: "active" } }] : []
|
|
8740
|
+
] });
|
|
8741
|
+
return next;
|
|
8742
|
+
} catch (error) {
|
|
8743
|
+
const code = error.code;
|
|
8744
|
+
if (code !== "transact_guard_failed" && code !== "unique_violation") throw error;
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8747
|
+
throw new Error("referral code could not be reissued");
|
|
8748
|
+
}
|
|
8749
|
+
|
|
8750
|
+
// src/referral-events.ts
|
|
8751
|
+
var REFERRAL_COOKIE = "odla_ref";
|
|
8752
|
+
function referralCodeFromCookie(header) {
|
|
8753
|
+
for (const part of (header ?? "").split(";")) {
|
|
8754
|
+
const [name, ...rest] = part.trim().split("=");
|
|
8755
|
+
if (name === REFERRAL_COOKIE) {
|
|
8756
|
+
const value = decodeURIComponent(rest.join("="));
|
|
8757
|
+
return REFERRAL_CODE_PATTERN.test(value) ? value : null;
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
return null;
|
|
8761
|
+
}
|
|
8762
|
+
|
|
8763
|
+
// src/referral-ledger.ts
|
|
8764
|
+
function foldLedger(rows3) {
|
|
8765
|
+
const sum = (status2) => rows3.filter((row2) => row2.status === status2).reduce((total, row2) => total + Number(row2.valueCents ?? 0), 0);
|
|
8766
|
+
const earnedCents = sum("earned"), appliedCents = sum("applied"), voidedCents = sum("void");
|
|
8767
|
+
return { earnedCents, appliedCents, voidedCents, carriedForwardCents: sum("carried_forward"), balanceCents: earnedCents - appliedCents - voidedCents };
|
|
8768
|
+
}
|
|
8769
|
+
|
|
8770
|
+
// src/worker-routes-member-referrals.ts
|
|
8771
|
+
var MEMBER_REFERRALS_PATH = "/api/member/referrals";
|
|
8772
|
+
|
|
8773
|
+
// src/worker-routes-referral-link.ts
|
|
8774
|
+
function membershipRedirect(membershipUrl2, origin, code) {
|
|
8775
|
+
const url = new URL(membershipUrl2, origin);
|
|
8776
|
+
if (code) url.searchParams.set("ref", code);
|
|
8777
|
+
return url.toString();
|
|
8778
|
+
}
|
|
8779
|
+
|
|
8780
|
+
// src/course-validate.ts
|
|
8781
|
+
var ID3 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8782
|
+
var KINDS2 = /* @__PURE__ */ new Set(["read", "do", "watch", "extra"]);
|
|
8783
|
+
var rec = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
8784
|
+
function text6(v, path, errors, max, required = false) {
|
|
8785
|
+
if (v === void 0 || v === null || v === "") {
|
|
8786
|
+
if (required) errors.push(`${path} is required`);
|
|
8787
|
+
return void 0;
|
|
8788
|
+
}
|
|
8789
|
+
if (typeof v !== "string") {
|
|
8790
|
+
errors.push(`${path} must be a string`);
|
|
8791
|
+
return void 0;
|
|
8792
|
+
}
|
|
8793
|
+
const t = v.trim();
|
|
8794
|
+
if (t.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
8795
|
+
return t || void 0;
|
|
8796
|
+
}
|
|
8797
|
+
function https(v, path, errors) {
|
|
8798
|
+
const t = text6(v, path, errors, 2e3);
|
|
8799
|
+
if (!t) return void 0;
|
|
8800
|
+
try {
|
|
8801
|
+
if (new URL(t).protocol === "https:") return t;
|
|
8802
|
+
} catch {
|
|
8803
|
+
}
|
|
8804
|
+
errors.push(`${path} must be an https URL`);
|
|
8805
|
+
return void 0;
|
|
8806
|
+
}
|
|
8807
|
+
function list(v, path, errors, max, item) {
|
|
8808
|
+
if (v === void 0) return [];
|
|
8809
|
+
if (!Array.isArray(v)) {
|
|
8810
|
+
errors.push(`${path} must be an array`);
|
|
8811
|
+
return [];
|
|
8812
|
+
}
|
|
8813
|
+
if (v.length > max) errors.push(`${path} supports at most ${max} entries`);
|
|
8814
|
+
return v.slice(0, max).flatMap((raw2, i) => {
|
|
8815
|
+
const r = rec(raw2);
|
|
8816
|
+
if (!r) {
|
|
8817
|
+
errors.push(`${path}[${i}] must be an object`);
|
|
8818
|
+
return [];
|
|
8819
|
+
}
|
|
8820
|
+
const out = item(r, `${path}[${i}]`);
|
|
8821
|
+
return out === void 0 ? [] : [out];
|
|
8822
|
+
});
|
|
8823
|
+
}
|
|
8824
|
+
var material = (errors, withKind) => (r, at2) => {
|
|
8825
|
+
if (withKind && !KINDS2.has(String(r.kind))) errors.push(`${at2}.kind must be read, do, watch or extra`);
|
|
8826
|
+
const label = text6(r.label, `${at2}.label`, errors, 200, true) ?? "";
|
|
8827
|
+
const meta = text6(r.meta, `${at2}.meta`, errors, 200), url = https(r.url, `${at2}.url`, errors);
|
|
8828
|
+
return { ...withKind ? { kind: r.kind } : {}, label, ...meta ? { meta } : {}, ...url ? { url } : {} };
|
|
8829
|
+
};
|
|
8830
|
+
function validateCourseSettings(input, enrolledCount = 0) {
|
|
8831
|
+
const errors = [];
|
|
8832
|
+
const r = rec(input);
|
|
8833
|
+
if (!r) return { ok: false, errors: ["course must be an object"] };
|
|
8834
|
+
const id12 = text6(r.id, "id", errors, 80, true) ?? "";
|
|
8835
|
+
if (id12 && !ID3.test(id12)) errors.push("id must be a lowercase slug");
|
|
8836
|
+
const title = text6(r.title, "title", errors, 200, true) ?? "";
|
|
8837
|
+
const timezone = text6(r.timezone, "timezone", errors, 64, true) ?? "";
|
|
8838
|
+
if (timezone) {
|
|
8839
|
+
try {
|
|
8840
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
8841
|
+
} catch {
|
|
8842
|
+
errors.push("timezone must be an IANA time zone");
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
const capacity = Number(r.capacity);
|
|
8846
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 100) errors.push("capacity must be an integer from 1 to 100");
|
|
8847
|
+
else if (capacity < enrolledCount) errors.push(`capacity cannot be below the ${enrolledCount} seats already taken`);
|
|
8848
|
+
const signupClosesAt = Number(r.signupClosesAt);
|
|
8849
|
+
if (!Number.isSafeInteger(signupClosesAt) || signupClosesAt <= 0) errors.push("signupClosesAt must be a timestamp");
|
|
8850
|
+
const c = rec(r.content ?? {}) ?? {};
|
|
8851
|
+
const outcomes = Array.isArray(c.outcomes) ? c.outcomes.slice(0, 12).map((o, i) => text6(o, `content.outcomes[${i}]`, errors, 300, true) ?? "") : [];
|
|
8852
|
+
const content = {};
|
|
8853
|
+
for (const key4 of ["basedOn", "short", "when", "start", "dates", "time"]) {
|
|
8854
|
+
const v = text6(c[key4], `content.${key4}`, errors, 600);
|
|
8855
|
+
if (v) content[key4] = v;
|
|
8856
|
+
}
|
|
8857
|
+
const partner = text6(c.partnerMarkdown, "content.partnerMarkdown", errors, 4e3);
|
|
8858
|
+
if (partner) content.partnerMarkdown = partner;
|
|
8859
|
+
if (outcomes.length) content.outcomes = outcomes;
|
|
8860
|
+
const sessions = list(r.sessions, "sessions", errors, 26, (s, at2) => {
|
|
8861
|
+
const startsAt = Number(s.startsAt), endsAt = Number(s.endsAt);
|
|
8862
|
+
if (!Number.isSafeInteger(startsAt) || !Number.isSafeInteger(endsAt) || endsAt <= startsAt) errors.push(`${at2} needs startsAt before endsAt`);
|
|
8863
|
+
const date = text6(s.date, `${at2}.date`, errors, 40, true) ?? "";
|
|
8864
|
+
const topic = text6(s.topic, `${at2}.topic`, errors, 200, true) ?? "";
|
|
8865
|
+
const description = text6(s.description, `${at2}.description`, errors, 1e3);
|
|
8866
|
+
return { date, startsAt, endsAt, topic, ...description ? { description } : {}, materials: list(s.materials, `${at2}.materials`, errors, 20, material(errors, true)) };
|
|
8867
|
+
});
|
|
8868
|
+
if (!sessions.length) errors.push("sessions must list at least one session");
|
|
8869
|
+
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}]`);
|
|
8870
|
+
const toolkit = list(r.toolkit, "toolkit", errors, 20, material(errors, false));
|
|
8871
|
+
const guides = list(r.guides, "guides", errors, 8, (g, at2) => {
|
|
8872
|
+
const name = text6(g.name, `${at2}.name`, errors, 120, true) ?? "";
|
|
8873
|
+
const role = text6(g.role, `${at2}.role`, errors, 200), bio = text6(g.bio, `${at2}.bio`, errors, 1500), photoUrl = https(g.photoUrl, `${at2}.photoUrl`, errors);
|
|
8874
|
+
return { name, ...role ? { role } : {}, ...bio ? { bio } : {}, ...photoUrl ? { photoUrl } : {} };
|
|
8875
|
+
});
|
|
8876
|
+
if (errors.length) return { ok: false, errors };
|
|
8877
|
+
return { ok: true, value: { id: id12, title, timezone, capacity, signupClosesAt, content, sessions, toolkit, guides } };
|
|
8878
|
+
}
|
|
8879
|
+
|
|
8880
|
+
// src/course-calendar.ts
|
|
8881
|
+
import { initCalendar } from "@odla-ai/calendar";
|
|
8882
|
+
function courseEventInput(course) {
|
|
8883
|
+
const first = course.sessions[0];
|
|
8884
|
+
return {
|
|
8885
|
+
summary: course.title,
|
|
8886
|
+
startAt: first.startsAt,
|
|
8887
|
+
endAt: first.endsAt,
|
|
8888
|
+
timezone: course.timezone,
|
|
8889
|
+
attendees: [],
|
|
8890
|
+
meet: true,
|
|
8891
|
+
recurrence: [`RRULE:FREQ=WEEKLY;COUNT=${course.sessions.length}`],
|
|
8892
|
+
guestsCanSeeOtherGuests: false,
|
|
8893
|
+
guestsCanInviteOthers: false,
|
|
8894
|
+
guestsCanModify: false,
|
|
8895
|
+
skipAvailabilityCheck: true
|
|
8896
|
+
};
|
|
8897
|
+
}
|
|
8898
|
+
|
|
8899
|
+
// src/course-store.ts
|
|
8900
|
+
var waitlistPosition = (enrollments, memberId) => {
|
|
8901
|
+
const index2 = enrollments.filter((row2) => row2.status === "waitlisted").findIndex((row2) => row2.memberId === memberId);
|
|
8902
|
+
return index2 < 0 ? null : index2 + 1;
|
|
8903
|
+
};
|
|
8904
|
+
|
|
8905
|
+
// src/worker-routes-member-course.ts
|
|
8906
|
+
function memberCourseView(course, enrollments, memberId, now = Date.now()) {
|
|
8907
|
+
const mine = enrollments.find((row2) => row2.memberId === memberId && ["enrolled", "waitlisted", "pending"].includes(String(row2.status)));
|
|
8908
|
+
const enrolled = mine?.status === "enrolled";
|
|
8909
|
+
const seatsLeft = Math.max(0, Number(course.capacity) - Number(course.enrolledCount));
|
|
8910
|
+
const visible = (material2) => enrolled ? material2 : (({ url: _url, ...rest }) => rest)(material2);
|
|
8911
|
+
const sessions = (course.sessions ?? []).map((session) => ({
|
|
8912
|
+
...session,
|
|
8913
|
+
materials: session.materials.filter((material2) => material2.url).map(visible)
|
|
8914
|
+
}));
|
|
8915
|
+
return {
|
|
8916
|
+
course: {
|
|
8917
|
+
id: course.id,
|
|
8918
|
+
title: course.title,
|
|
8919
|
+
timezone: course.timezone,
|
|
8920
|
+
content: course.content ?? {},
|
|
8921
|
+
guides: course.guides ?? [],
|
|
8922
|
+
capacity: Number(course.capacity),
|
|
8923
|
+
seatsLeft,
|
|
8924
|
+
signupClosesAt: course.signupClosesAt,
|
|
8925
|
+
sessions,
|
|
8926
|
+
toolkit: (course.toolkit ?? []).filter((item) => item.url).map(visible),
|
|
8927
|
+
status: now >= Number(course.signupClosesAt) ? "closed" : seatsLeft === 0 ? "full" : "open"
|
|
8928
|
+
},
|
|
8929
|
+
enrollment: mine ? { status: mine.status, waitlistPosition: waitlistPosition(enrollments, memberId) } : null,
|
|
8930
|
+
...enrolled && course.meetUrl ? { meetUrl: course.meetUrl } : {}
|
|
8931
|
+
};
|
|
8932
|
+
}
|
|
8933
|
+
|
|
8602
8934
|
// src/email-retirement.ts
|
|
8603
|
-
function isRetiredEmailCopy(subject,
|
|
8935
|
+
function isRetiredEmailCopy(subject, text9) {
|
|
8604
8936
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
8605
|
-
const bodyCopy =
|
|
8937
|
+
const bodyCopy = text9.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
8606
8938
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
8607
8939
|
}
|
|
8608
8940
|
|
|
@@ -8627,8 +8959,8 @@ function lineHtml(line) {
|
|
|
8627
8959
|
}
|
|
8628
8960
|
return out + escapeHtml(line.slice(last));
|
|
8629
8961
|
}
|
|
8630
|
-
function textToHtml(
|
|
8631
|
-
const paragraphs =
|
|
8962
|
+
function textToHtml(text9) {
|
|
8963
|
+
const paragraphs = text9.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
8632
8964
|
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
8633
8965
|
return `<!doctype html>
|
|
8634
8966
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
@@ -8733,12 +9065,12 @@ function planDelivery(input) {
|
|
|
8733
9065
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
8734
9066
|
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
8735
9067
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
8736
|
-
const
|
|
9068
|
+
const text9 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
8737
9069
|
|
|
8738
9070
|
` + renderedText : renderedText;
|
|
8739
9071
|
const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
|
|
8740
9072
|
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;
|
|
8741
|
-
return { deliver: true, transport, to, subject, text:
|
|
9073
|
+
return { deliver: true, transport, to, subject, text: text9, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
|
|
8742
9074
|
}
|
|
8743
9075
|
|
|
8744
9076
|
// src/email-readiness.ts
|
|
@@ -8778,7 +9110,7 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
|
8778
9110
|
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
8779
9111
|
function emailSpamSignals(copy) {
|
|
8780
9112
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
8781
|
-
const
|
|
9113
|
+
const text9 = typeof copy.text === "string" ? copy.text : "";
|
|
8782
9114
|
const html = typeof copy.html === "string" ? copy.html : "";
|
|
8783
9115
|
const out = [];
|
|
8784
9116
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
@@ -8788,11 +9120,11 @@ function emailSpamSignals(copy) {
|
|
|
8788
9120
|
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." });
|
|
8789
9121
|
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
8790
9122
|
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
8791
|
-
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(
|
|
9123
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text9));
|
|
8792
9124
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
8793
|
-
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(
|
|
8794
|
-
if (/!{2,}/.test(
|
|
8795
|
-
const urls = [...`${
|
|
9125
|
+
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." });
|
|
9126
|
+
if (/!{2,}/.test(text9)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
9127
|
+
const urls = [...`${text9}
|
|
8796
9128
|
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
8797
9129
|
const hosts = urls.map((url) => {
|
|
8798
9130
|
try {
|
|
@@ -8805,7 +9137,7 @@ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
|
8805
9137
|
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." });
|
|
8806
9138
|
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." });
|
|
8807
9139
|
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." });
|
|
8808
|
-
if (urls.length && words(
|
|
9140
|
+
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." });
|
|
8809
9141
|
return out;
|
|
8810
9142
|
}
|
|
8811
9143
|
function emailSenderWarnings(input) {
|
|
@@ -9009,14 +9341,14 @@ async function sendTemplated(deps, input) {
|
|
|
9009
9341
|
if (decision.transport === "log-only") return { sent: true };
|
|
9010
9342
|
let messageId;
|
|
9011
9343
|
try {
|
|
9012
|
-
const
|
|
9013
|
-
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(
|
|
9344
|
+
const text9 = String(row2.body);
|
|
9345
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text9);
|
|
9014
9346
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
9015
9347
|
const response = await deps.sender.send({
|
|
9016
9348
|
from: fromAddress(deps.from, input.group.fromName),
|
|
9017
9349
|
to: [decision.to],
|
|
9018
9350
|
subject: String(row2.subject),
|
|
9019
|
-
text:
|
|
9351
|
+
text: text9,
|
|
9020
9352
|
...html ? { html } : {},
|
|
9021
9353
|
...headers ? { headers } : {},
|
|
9022
9354
|
replyTo: input.group.replyTo
|
|
@@ -9268,7 +9600,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
|
|
|
9268
9600
|
}
|
|
9269
9601
|
|
|
9270
9602
|
// src/admission-grants.ts
|
|
9271
|
-
var
|
|
9603
|
+
var ID4 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
9272
9604
|
var MAX_GRANTS = 2e3;
|
|
9273
9605
|
var MAX_NOTE = 2e3;
|
|
9274
9606
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -9290,10 +9622,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
9290
9622
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
9291
9623
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
9292
9624
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
9293
|
-
if (!
|
|
9625
|
+
if (!ID4.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
9294
9626
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
9295
|
-
if (!
|
|
9296
|
-
if (!
|
|
9627
|
+
if (!ID4.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
9628
|
+
if (!ID4.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
9297
9629
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
9298
9630
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
9299
9631
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -9308,7 +9640,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
9308
9640
|
const decision = grant.decision;
|
|
9309
9641
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
9310
9642
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
9311
|
-
if (!
|
|
9643
|
+
if (!ID4.test(id12)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
9312
9644
|
if (seenIds.has(id12)) errors.push(`document.grants repeats id "${id12}"`);
|
|
9313
9645
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
9314
9646
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -9467,11 +9799,11 @@ function resolveJoinContinuation(input) {
|
|
|
9467
9799
|
// src/gift-offers.ts
|
|
9468
9800
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
9469
9801
|
var MAX_GIFT_OFFERS = 20;
|
|
9470
|
-
var
|
|
9802
|
+
var ID5 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
9471
9803
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
9472
9804
|
var CURRENCY = /^[a-z]{3}$/;
|
|
9473
9805
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
9474
|
-
var
|
|
9806
|
+
var text7 = (value, path, errors, max) => {
|
|
9475
9807
|
if (typeof value !== "string" || !value.trim()) {
|
|
9476
9808
|
errors.push(`${path} is required`);
|
|
9477
9809
|
return "";
|
|
@@ -9499,23 +9831,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
9499
9831
|
}
|
|
9500
9832
|
const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
9501
9833
|
for (const key4 of Object.keys(item)) if (!allowed.has(key4)) errors.push(`${path}.${key4} is not supported`);
|
|
9502
|
-
const id12 =
|
|
9503
|
-
if (id12 && !
|
|
9834
|
+
const id12 = text7(item.id, `${path}.id`, errors, 80);
|
|
9835
|
+
if (id12 && !ID5.test(id12)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9504
9836
|
if (ids2.has(id12)) errors.push(`${path}.id duplicates "${id12}"`);
|
|
9505
9837
|
ids2.add(id12);
|
|
9506
9838
|
const targetRaw = record3(item.target);
|
|
9507
9839
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
9508
9840
|
if (targetRaw?.type === "tier") {
|
|
9509
|
-
const tierId =
|
|
9841
|
+
const tierId = text7(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
9510
9842
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
9511
9843
|
target = { type: "tier", tierId };
|
|
9512
9844
|
} else if (targetRaw?.type === "entitlement") {
|
|
9513
|
-
const entitlementKey =
|
|
9514
|
-
if (entitlementKey && !
|
|
9845
|
+
const entitlementKey = text7(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
9846
|
+
if (entitlementKey && !ID5.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
9515
9847
|
target = { type: "entitlement", entitlementKey };
|
|
9516
9848
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
9517
9849
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
9518
|
-
const stripePriceId =
|
|
9850
|
+
const stripePriceId = text7(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
9519
9851
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
9520
9852
|
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`);
|
|
9521
9853
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -9537,9 +9869,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
9537
9869
|
redemptionLifetimeHours: lifetime,
|
|
9538
9870
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
9539
9871
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
9540
|
-
title:
|
|
9541
|
-
description:
|
|
9542
|
-
ctaLabel:
|
|
9872
|
+
title: text7(item.title, `${path}.title`, errors, 160),
|
|
9873
|
+
description: text7(item.description, `${path}.description`, errors, 2e3),
|
|
9874
|
+
ctaLabel: text7(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
9543
9875
|
});
|
|
9544
9876
|
}
|
|
9545
9877
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -10322,10 +10654,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
10322
10654
|
errors.push(`${path} must be a string`);
|
|
10323
10655
|
return void 0;
|
|
10324
10656
|
}
|
|
10325
|
-
const
|
|
10326
|
-
if (required && !
|
|
10327
|
-
if (
|
|
10328
|
-
return
|
|
10657
|
+
const text9 = value.trim();
|
|
10658
|
+
if (required && !text9) errors.push(`${path} is required`);
|
|
10659
|
+
if (text9.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
10660
|
+
return text9;
|
|
10329
10661
|
}
|
|
10330
10662
|
function validateMembershipPageDocument(input, options = {}) {
|
|
10331
10663
|
const errors = [];
|
|
@@ -10374,8 +10706,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
10374
10706
|
else {
|
|
10375
10707
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
10376
10708
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
10377
|
-
const
|
|
10378
|
-
return
|
|
10709
|
+
const text9 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
10710
|
+
return text9 ? [text9] : [];
|
|
10379
10711
|
});
|
|
10380
10712
|
}
|
|
10381
10713
|
}
|
|
@@ -10474,7 +10806,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
10474
10806
|
function unknownKeys3(value, allowed, path, errors) {
|
|
10475
10807
|
for (const key4 of Object.keys(value)) if (!allowed.has(key4)) errors.push(`${path}.${key4} is not supported`);
|
|
10476
10808
|
}
|
|
10477
|
-
function
|
|
10809
|
+
function text8(value, path, errors, max = 65536) {
|
|
10478
10810
|
if (typeof value !== "string" || !value.trim()) {
|
|
10479
10811
|
errors.push(`${path} is required`);
|
|
10480
10812
|
return "";
|
|
@@ -10497,11 +10829,11 @@ function tiersOf(value, errors) {
|
|
|
10497
10829
|
return [];
|
|
10498
10830
|
}
|
|
10499
10831
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
10500
|
-
const id12 =
|
|
10832
|
+
const id12 = text8(item.id, `${path}.id`, errors, 80);
|
|
10501
10833
|
if (id12 && !TIER.test(id12)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
10502
10834
|
if (seen.has(id12)) errors.push(`${path}.id duplicates "${id12}"`);
|
|
10503
10835
|
seen.add(id12);
|
|
10504
|
-
const name =
|
|
10836
|
+
const name = text8(item.name, `${path}.name`, errors, 160);
|
|
10505
10837
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
10506
10838
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
10507
10839
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -10519,7 +10851,7 @@ function tiersOf(value, errors) {
|
|
|
10519
10851
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
10520
10852
|
errors.push(`${path}.interval must be year or month`);
|
|
10521
10853
|
}
|
|
10522
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
10854
|
+
const blurb = item.blurb === void 0 ? void 0 : text8(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
10523
10855
|
return [{
|
|
10524
10856
|
id: id12,
|
|
10525
10857
|
name,
|
|
@@ -10577,7 +10909,7 @@ function policyOf(value, errors, tiers2) {
|
|
|
10577
10909
|
const result = {};
|
|
10578
10910
|
for (const key4 of allowed) {
|
|
10579
10911
|
if (input[key4] === void 0 && key4 !== "disclaimerText" && key4 !== "refundPolicyText") continue;
|
|
10580
|
-
result[key4] =
|
|
10912
|
+
result[key4] = text8(input[key4], `document.policy.${key4}`, errors);
|
|
10581
10913
|
}
|
|
10582
10914
|
const tierPolicies = {};
|
|
10583
10915
|
if (input.tierPolicies !== void 0) {
|
|
@@ -10592,8 +10924,8 @@ function policyOf(value, errors, tiers2) {
|
|
|
10592
10924
|
}
|
|
10593
10925
|
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
10594
10926
|
tierPolicies[id12] = {
|
|
10595
|
-
refundPolicyText:
|
|
10596
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
10927
|
+
refundPolicyText: text8(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
10928
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text8(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
10597
10929
|
};
|
|
10598
10930
|
}
|
|
10599
10931
|
}
|
|
@@ -10633,9 +10965,9 @@ function validateSignupControlDocument(input) {
|
|
|
10633
10965
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
10634
10966
|
const source = recordOf(root.source);
|
|
10635
10967
|
const target = recordOf(root.target);
|
|
10636
|
-
const appId =
|
|
10637
|
-
const chapterId =
|
|
10638
|
-
const environment =
|
|
10968
|
+
const appId = text8(source?.appId, "document.source.appId", errors, 63);
|
|
10969
|
+
const chapterId = text8(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
10970
|
+
const environment = text8(target?.environment, "document.target.environment", errors, 63);
|
|
10639
10971
|
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
10640
10972
|
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
10641
10973
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -10812,9 +11144,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
10812
11144
|
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
10813
11145
|
continue;
|
|
10814
11146
|
}
|
|
10815
|
-
const
|
|
10816
|
-
if (
|
|
10817
|
-
if (
|
|
11147
|
+
const text9 = (match[1] ?? "").trim();
|
|
11148
|
+
if (text9.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
11149
|
+
if (text9) benefits.push(text9);
|
|
10818
11150
|
}
|
|
10819
11151
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
10820
11152
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -11173,6 +11505,7 @@ export {
|
|
|
11173
11505
|
MEMBER_PHOTO_PATH,
|
|
11174
11506
|
MEMBER_PROFILE_FIELDS,
|
|
11175
11507
|
MEMBER_PROFILE_PATH,
|
|
11508
|
+
MEMBER_REFERRALS_PATH,
|
|
11176
11509
|
MEMBER_SELF_EDITABLE_FIELDS,
|
|
11177
11510
|
MEMBER_SELF_READABLE_FIELDS,
|
|
11178
11511
|
MEMBER_SYSTEM_AUTHORITY_MATRIX,
|
|
@@ -11180,6 +11513,9 @@ export {
|
|
|
11180
11513
|
MEMBER_SYSTEM_RECORD_OWNERS,
|
|
11181
11514
|
MEMBER_SYSTEM_RECOVERY_OBJECTIVES,
|
|
11182
11515
|
PHOTO_SIZE,
|
|
11516
|
+
REFERRAL_CODE_ALPHABET,
|
|
11517
|
+
REFERRAL_CODE_PATTERN,
|
|
11518
|
+
REFERRAL_COOKIE,
|
|
11183
11519
|
REQUIRED_NOTICE_KINDS,
|
|
11184
11520
|
SCHEDULING_DEFAULTS,
|
|
11185
11521
|
SIGNUP_CONTROL_PATH,
|
|
@@ -11234,6 +11570,7 @@ export {
|
|
|
11234
11570
|
composeMemberContent,
|
|
11235
11571
|
composeMemberSections,
|
|
11236
11572
|
confirmMembershipPayment,
|
|
11573
|
+
courseEventInput,
|
|
11237
11574
|
createAdmissionGrantEnvelope,
|
|
11238
11575
|
createBuilderLibraryEnvelope,
|
|
11239
11576
|
createChapterIntegration,
|
|
@@ -11254,12 +11591,15 @@ export {
|
|
|
11254
11591
|
emailSenderWarnings,
|
|
11255
11592
|
emailSpamSignals,
|
|
11256
11593
|
endForSlot,
|
|
11594
|
+
ensureReferralCode,
|
|
11595
|
+
entitledTierId,
|
|
11257
11596
|
exportChapterPromotion,
|
|
11258
11597
|
findAdmissionGrant,
|
|
11259
11598
|
findApplicationRef,
|
|
11260
11599
|
findGiftOffer,
|
|
11261
11600
|
findTier,
|
|
11262
11601
|
firstPaymentPatch,
|
|
11602
|
+
foldLedger,
|
|
11263
11603
|
formatChapterCopy,
|
|
11264
11604
|
formationFields,
|
|
11265
11605
|
formationFlowFields,
|
|
@@ -11293,6 +11633,7 @@ export {
|
|
|
11293
11633
|
memberApplication,
|
|
11294
11634
|
memberContentDigest,
|
|
11295
11635
|
memberContentDocument,
|
|
11636
|
+
memberCourseView,
|
|
11296
11637
|
memberFeatureAccess,
|
|
11297
11638
|
memberPageAllowed,
|
|
11298
11639
|
memberPortalEntities,
|
|
@@ -11309,12 +11650,14 @@ export {
|
|
|
11309
11650
|
membershipOfferRules,
|
|
11310
11651
|
membershipOfferingVersion,
|
|
11311
11652
|
membershipPageDocument,
|
|
11653
|
+
membershipRedirect,
|
|
11312
11654
|
membershipTierPresentationFromMarkdown,
|
|
11313
11655
|
nameKey,
|
|
11314
11656
|
namedSeatOfferDigest,
|
|
11315
11657
|
networkCommercialSourceKey,
|
|
11316
11658
|
networkMembershipId,
|
|
11317
11659
|
networkSourceTag,
|
|
11660
|
+
newReferralCode,
|
|
11318
11661
|
normalizeAdmissionEmail,
|
|
11319
11662
|
normalizeChapterEmail,
|
|
11320
11663
|
normalizeNetworkCommercialStripeEvent,
|
|
@@ -11348,8 +11691,10 @@ export {
|
|
|
11348
11691
|
reconcileMembershipBilling,
|
|
11349
11692
|
reconcileMembershipRenewal,
|
|
11350
11693
|
reconcileStripeSnapshot,
|
|
11694
|
+
referralCodeFromCookie,
|
|
11351
11695
|
refundNetworkMembership,
|
|
11352
11696
|
refundedPatch,
|
|
11697
|
+
reissueReferralCode,
|
|
11353
11698
|
render,
|
|
11354
11699
|
renderHtmlBody,
|
|
11355
11700
|
renderSignupControlThemeHead,
|
|
@@ -11415,6 +11760,7 @@ export {
|
|
|
11415
11760
|
updateClerkUserMetadataByEmail,
|
|
11416
11761
|
validateAdmissionGrantDocument,
|
|
11417
11762
|
validateBuilderLibraryDocument,
|
|
11763
|
+
validateCourseSettings,
|
|
11418
11764
|
validateEmailReadiness,
|
|
11419
11765
|
validateGiftOfferCatalog,
|
|
11420
11766
|
validateMemberContentDocument,
|