@odla-ai/chapter 0.62.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 text8 = id12.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1786
- return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id12;
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 list = await call("GET", "/v1/invoices", {
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(list.data) || typeof list.has_more !== "boolean") fail3("stripe_invalid_response");
4389
- for (const value of list.data) {
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 (!list.has_more) break;
4397
- if (!list.data.length || page === 9) fail3("refund_history_incomplete");
4398
- cursor = id8(list.data[list.data.length - 1]);
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 list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
5635
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
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 list.data) {
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 list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
5690
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
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 list.data) {
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 text8 = "";
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
- text8 += decoder.decode(value, { stream: true });
6470
+ text9 += decoder.decode(value, { stream: true });
6385
6471
  }
6386
6472
  try {
6387
- return JSON.parse(text8 + decoder.decode());
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 list = rows3.map((row2) => rowToTier(row2, groupId));
7956
- return resolveTiers(group, list);
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 text8 = clean(value);
8323
- if (text8.length > max) errors.push(`${key4} must be at most ${max} characters`);
8324
- return text8 || null;
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, text8) {
8935
+ function isRetiredEmailCopy(subject, text9) {
8604
8936
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
8605
- const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
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(text8) {
8631
- const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
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 text8 = redirect ? `(dev redirect; original recipient: ${input.to})
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: text8, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
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 text8 = typeof copy.text === "string" ? copy.text : "";
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(text8));
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(text8))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
8794
- if (/!{2,}/.test(text8)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
8795
- const urls = [...`${text8}
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(/&quot;.*$/, ""));
8797
9129
  const hosts = urls.map((url) => {
8798
9130
  try {
@@ -8805,7 +9137,7 @@ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
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(text8).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." });
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 text8 = String(row2.body);
9013
- const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text8);
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: text8,
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 ID3 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
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 (!ID3.test(sourceId)) errors.push("document.source.appId must be an identifier");
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 (!ID3.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
9296
- if (!ID3.test(environment)) errors.push("document.target.environment must be an identifier");
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 (!ID3.test(id12)) errors.push(`document.grants[${index2}].id must be an identifier`);
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 ID4 = /^[a-z][a-z0-9-]{1,79}$/;
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 text6 = (value, path, errors, max) => {
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 = text6(item.id, `${path}.id`, errors, 80);
9503
- if (id12 && !ID4.test(id12)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 = text6(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
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 = text6(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
9514
- if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
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 = text6(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
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: text6(item.title, `${path}.title`, errors, 160),
9541
- description: text6(item.description, `${path}.description`, errors, 2e3),
9542
- ctaLabel: text6(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
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 text8 = value.trim();
10326
- if (required && !text8) errors.push(`${path} is required`);
10327
- if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
10328
- return text8;
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 text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
10378
- return text8 ? [text8] : [];
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 text7(value, path, errors, max = 65536) {
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 = text7(item.id, `${path}.id`, errors, 80);
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 = text7(item.name, `${path}.name`, errors, 160);
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 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
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] = text7(input[key4], `document.policy.${key4}`, errors);
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: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
10596
- ...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
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 = text7(source?.appId, "document.source.appId", errors, 63);
10637
- const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
10638
- const environment = text7(target?.environment, "document.target.environment", errors, 63);
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 text8 = (match[1] ?? "").trim();
10816
- if (text8.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
10817
- if (text8) benefits.push(text8);
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,