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