@odla-ai/chapter 0.59.1 → 0.61.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
@@ -1397,12 +1397,15 @@ var DEFAULT_ADMIN_COPY = {
1397
1397
  replyTo: "Reply-to",
1398
1398
  fromName: "From name",
1399
1399
  fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
1400
+ spamRisk: "Spam-filter risk",
1400
1401
  debugInbox: "Debug inbox",
1401
1402
  debugInboxHint: "Non-production sends redirect here.",
1402
1403
  sendTest: "Send test",
1403
1404
  enabled: "Enabled",
1404
1405
  subject: "Subject",
1405
1406
  body: "Body",
1407
+ htmlBody: "HTML body (optional)",
1408
+ htmlBodyHint: "Sent beside the plain text when present, with the same {{placeholders}} and {{#name}} sections; values are escaped. Leave empty to send the plain text rendered as HTML.",
1406
1409
  save: "Save changes",
1407
1410
  sendLog: "Send log",
1408
1411
  sentColumn: "Sent",
@@ -7336,18 +7339,72 @@ function isRetiredEmailCopy(subject, text8) {
7336
7339
  // src/email.ts
7337
7340
  import { validateEmailCopy } from "@odla-ai/crm";
7338
7341
 
7342
+ // src/email-render.ts
7343
+ import { renderTemplate, templateLiveVars, templateSectionIssues, templateSectionNames, templateVars } from "@odla-ai/crm";
7344
+
7345
+ // src/email-html.ts
7346
+ var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
7347
+ function escapeHtml(s) {
7348
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7349
+ }
7350
+ function lineHtml(line) {
7351
+ let out = "", last = 0;
7352
+ for (const match of line.matchAll(URL_RE)) {
7353
+ out += escapeHtml(line.slice(last, match.index));
7354
+ const url = escapeHtml(match[0]);
7355
+ out += `<a href="${url}">${url}</a>`;
7356
+ last = match.index + match[0].length;
7357
+ }
7358
+ return out + escapeHtml(line.slice(last));
7359
+ }
7360
+ function textToHtml(text8) {
7361
+ const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
7362
+ const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
7363
+ return `<!doctype html>
7364
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
7365
+ <body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
7366
+ ${body}
7367
+ </body></html>`;
7368
+ }
7369
+
7370
+ // src/email-render.ts
7371
+ function render(template, vars) {
7372
+ return renderTemplate(template, vars);
7373
+ }
7374
+ var HTML_BODY_LIMIT = 2e5;
7375
+ function renderHtmlBody(template, vars) {
7376
+ const escaped = Object.fromEntries(Object.entries(vars).map(([name, value]) => [name, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
7377
+ return renderTemplate(template, escaped);
7378
+ }
7379
+ function htmlBodyIssues(html, options) {
7380
+ const issues = [];
7381
+ if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
7382
+ for (const message of templateSectionIssues(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
7383
+ const unknown = new Set(templateVars(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
7384
+ for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
7385
+ if (options.values) {
7386
+ for (const name of templateSectionNames(html)) if (typeof options.values[name] !== "string") issues.push({ code: "value-missing", variable: name, message: `Missing event value: ${name} (HTML body).` });
7387
+ for (const variable of templateLiveVars(html, options.values)) {
7388
+ if (unknown.has(variable)) continue;
7389
+ const value = options.values[variable];
7390
+ if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
7391
+ }
7392
+ }
7393
+ return issues;
7394
+ }
7395
+
7339
7396
  // src/email-events.ts
7340
7397
  var identity = ["firstName", "lastName", "email", "phone", "state"];
7341
7398
  var policy = ["refundPolicyText", "commitmentText", "normsText"];
7342
- var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
7343
- var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
7399
+ var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
7400
+ var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
7344
7401
  var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
7345
7402
  var CHAPTER_EMAIL_EVENTS = {
7346
7403
  adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
7347
7404
  submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
7348
7405
  paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
7349
7406
  prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
7350
- onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
7407
+ onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
7351
7408
  namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
7352
7409
  seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
7353
7410
  };
@@ -7362,9 +7419,6 @@ function chapterEmailEvents(chapter) {
7362
7419
  }
7363
7420
 
7364
7421
  // src/email.ts
7365
- function render(template, vars) {
7366
- return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
7367
- }
7368
7422
  function groupVars(group, vars) {
7369
7423
  return {
7370
7424
  ...vars,
@@ -7397,7 +7451,9 @@ function planDelivery(input) {
7397
7451
  values: vars,
7398
7452
  tierId: vars.tierId
7399
7453
  });
7400
- if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
7454
+ const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
7455
+ const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
7456
+ if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
7401
7457
  const isProd = input.envName === "prod";
7402
7458
  const redirect = !isProd && !!input.group.debugEmail;
7403
7459
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
@@ -7410,11 +7466,93 @@ function planDelivery(input) {
7410
7466
  const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
7411
7467
 
7412
7468
  ` + renderedText : renderedText;
7413
- return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
7469
+ const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
7470
+ 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;
7471
+ return { deliver: true, transport, to, subject, text: text8, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
7472
+ }
7473
+
7474
+ // src/email-readiness.ts
7475
+ import { isEmailTierName, templateSectionNames as templateSectionNames2, templateVars as templateVars2, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
7476
+
7477
+ // src/email-spam-signals.ts
7478
+ var TRIGGER_PHRASES = [
7479
+ "act now",
7480
+ "click here",
7481
+ "limited time",
7482
+ "guaranteed",
7483
+ "risk-free",
7484
+ "risk free",
7485
+ "no obligation",
7486
+ "100% free",
7487
+ "winner",
7488
+ "congratulations",
7489
+ "urgent",
7490
+ "double your",
7491
+ "earn money",
7492
+ "make money",
7493
+ "cash bonus",
7494
+ "once in a lifetime",
7495
+ "exclusive deal",
7496
+ "order now",
7497
+ "buy now",
7498
+ "this is not spam",
7499
+ "dear friend",
7500
+ "dear customer"
7501
+ ];
7502
+ var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
7503
+ var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
7504
+ var SHORTENERS = /^(?:bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|cutt\.ly|tiny\.cc|shorturl\.at|rb\.gy)$/i;
7505
+ var URL_RE2 = /https?:\/\/[^\s<>"')\]]+/g;
7506
+ var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
7507
+ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
7508
+ var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
7509
+ function emailSpamSignals(copy) {
7510
+ const subject = typeof copy.subject === "string" ? copy.subject : "";
7511
+ const text8 = typeof copy.text === "string" ? copy.text : "";
7512
+ const html = typeof copy.html === "string" ? copy.html : "";
7513
+ const out = [];
7514
+ const letters = strip(subject).replace(/[^A-Za-z]/g, "");
7515
+ const shouting = letters.length >= 6 && letters.replace(/[^A-Z]/g, "").length / letters.length > 0.5 || words(subject).some((word) => /^[A-Z]{4,}$/.test(word.replace(/[^A-Za-z]/g, "")));
7516
+ if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
7517
+ if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
7518
+ 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." });
7519
+ const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
7520
+ if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
7521
+ const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text8));
7522
+ if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
7523
+ 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." });
7524
+ if (/!{2,}/.test(text8)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
7525
+ const urls = [...`${text8}
7526
+ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
7527
+ const hosts = urls.map((url) => {
7528
+ try {
7529
+ return new URL(url).hostname;
7530
+ } catch {
7531
+ return "";
7532
+ }
7533
+ });
7534
+ if (urls.some((url) => /^http:\/\//i.test(url))) out.push({ code: "link-insecure", message: "A link uses http:// instead of https://. Filters flag insecure links." });
7535
+ 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." });
7536
+ 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." });
7537
+ 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." });
7538
+ 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." });
7539
+ return out;
7540
+ }
7541
+ function emailSenderWarnings(input) {
7542
+ const out = [];
7543
+ const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
7544
+ const reply = (input.replyTo ?? "").trim().toLowerCase();
7545
+ const domain = (address) => address.split("@")[1] ?? "";
7546
+ if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
7547
+ out.push({ code: "sender-no-reply", message: "Mail is sent from a no-reply address. Gmail's sender guidelines advise against it; send from a monitored address so replies and unsubscribe requests reach someone." });
7548
+ }
7549
+ if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
7550
+ out.push({ code: "reply-to-domain", message: `Replies go to ${domain(reply)} while mail is sent from ${domain(from)}. A Reply-To on another domain is a mild spam signal; prefer an address on the sending domain.` });
7551
+ }
7552
+ return out;
7414
7553
  }
7415
7554
 
7416
7555
  // src/email-readiness.ts
7417
- import { isEmailTierName, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
7418
7556
  function validateEmailReadiness(input) {
7419
7557
  const issues = [];
7420
7558
  for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
@@ -7430,18 +7568,44 @@ function validateEmailReadiness(input) {
7430
7568
  const template = input.templates[key3];
7431
7569
  const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy2(template, { allowedVariables: event2.variables });
7432
7570
  if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
7571
+ const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
7572
+ if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
7433
7573
  if (template) {
7434
- const used = [...`${template.subject ?? ""}
7435
- ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
7436
- for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
7574
+ const copy = `${template.subject ?? ""}
7575
+ ${template.text ?? ""}
7576
+ ${html}`;
7577
+ const sectionOnly = new Set(templateSectionNames2(copy));
7578
+ for (const name of templateVars2(copy)) if (name in input.groupValues && !input.groupValues[name]?.trim() && !sectionOnly.has(name)) {
7437
7579
  problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
7438
7580
  }
7439
7581
  }
7440
7582
  if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
7441
7583
  else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
7442
- return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
7584
+ return {
7585
+ ...event2,
7586
+ key: key3,
7587
+ status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
7588
+ issues: problems,
7589
+ warnings: template ? emailSpamSignals(template) : []
7590
+ };
7443
7591
  });
7444
- return { checkedAt: input.now ?? Date.now(), events: events2, issues };
7592
+ return { checkedAt: input.now ?? Date.now(), events: events2, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
7593
+ }
7594
+
7595
+ // src/email-deliverability.ts
7596
+ var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
7597
+ function isRecipientFacing(event2) {
7598
+ return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
7599
+ }
7600
+ function listUnsubscribeHeader(replyTo) {
7601
+ const address = replyTo.trim();
7602
+ if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
7603
+ return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
7604
+ }
7605
+ function deliverabilityHeaders(input) {
7606
+ if (!isRecipientFacing(input.event)) return void 0;
7607
+ const value = listUnsubscribeHeader(input.replyTo);
7608
+ return value ? { "List-Unsubscribe": value } : void 0;
7445
7609
  }
7446
7610
 
7447
7611
  // src/notify.ts
@@ -7499,10 +7663,14 @@ async function frozenCheckoutVariables(db, app) {
7499
7663
  if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
7500
7664
  formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
7501
7665
  }
7666
+ const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
7667
+ const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
7502
7668
  return {
7503
7669
  paymentAmount: formatted,
7504
7670
  paymentAmountSummary: formatted ? ` of ${formatted}` : "",
7505
- tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
7671
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
7672
+ namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
7673
+ namedSeatRecipientName
7506
7674
  };
7507
7675
  }
7508
7676
  async function catalogTierName(db, group, app) {
@@ -7609,11 +7777,16 @@ async function sendTemplated(deps, input) {
7609
7777
  if (decision.transport === "log-only") return { sent: true };
7610
7778
  let messageId;
7611
7779
  try {
7780
+ const text8 = String(row2.body);
7781
+ const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text8);
7782
+ const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
7612
7783
  const response = await deps.sender.send({
7613
7784
  from: fromAddress(deps.from, input.group.fromName),
7614
7785
  to: [decision.to],
7615
7786
  subject: String(row2.subject),
7616
- text: String(row2.body),
7787
+ text: text8,
7788
+ ...html ? { html } : {},
7789
+ ...headers ? { headers } : {},
7617
7790
  replyTo: input.group.replyTo
7618
7791
  });
7619
7792
  if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
@@ -10372,6 +10545,7 @@ function createMembershipAuthorityIntegration() {
10372
10545
  };
10373
10546
  }
10374
10547
  export {
10548
+ ADMINISTRATOR_AUDIENCE,
10375
10549
  ADMISSION_GRANTS_PATH,
10376
10550
  ADMISSION_GRANTS_SCHEMA_VERSION,
10377
10551
  BUILDER_LIBRARY_ADMIN_PATH,
@@ -10386,6 +10560,7 @@ export {
10386
10560
  DEFAULT_SHARE_FIELDS,
10387
10561
  EDGE_UNPAIRED,
10388
10562
  GIFT_OFFER_CATALOG_VERSION,
10563
+ HTML_BODY_LIMIT,
10389
10564
  JOURNEY_ENDINGS,
10390
10565
  JOURNEY_STAGES,
10391
10566
  MAX_GIFT_OFFERS,
@@ -10438,6 +10613,7 @@ export {
10438
10613
  chapterDb,
10439
10614
  chapterEmailEvents,
10440
10615
  chapterFlowFieldContract,
10616
+ checkoutVariables,
10441
10617
  clampArray,
10442
10618
  classifyMemberStripeEvent,
10443
10619
  classifyMemberSystemWrite,
@@ -10465,9 +10641,12 @@ export {
10465
10641
  dashboardMetricData,
10466
10642
  defaultCrm,
10467
10643
  defineChapter,
10644
+ deliverabilityHeaders,
10468
10645
  drainMembershipEffects,
10469
10646
  edgeUnpaired,
10470
10647
  emailGroupFrom,
10648
+ emailSenderWarnings,
10649
+ emailSpamSignals,
10471
10650
  endForSlot,
10472
10651
  exportChapterPromotion,
10473
10652
  findAdmissionGrant,
@@ -10478,14 +10657,17 @@ export {
10478
10657
  formatChapterCopy,
10479
10658
  formationFields,
10480
10659
  formationFlowFields,
10660
+ frozenCheckoutVariables,
10481
10661
  getVaultSecret,
10482
10662
  giftOfferActive,
10483
10663
  hasDisclaimerAck,
10664
+ htmlBodyIssues,
10484
10665
  hydrateSignupControlTheme,
10485
10666
  injectSignupControlThemeHead,
10486
10667
  introIdempotencyKey,
10487
10668
  isAdminRole,
10488
10669
  isAlreadySent,
10670
+ isRecipientFacing,
10489
10671
  isReconcilable,
10490
10672
  isSlotAvailable,
10491
10673
  isValidEmail,
@@ -10494,6 +10676,7 @@ export {
10494
10676
  joinConfigTiers,
10495
10677
  leaderCrmConfig,
10496
10678
  listStripeObjects,
10679
+ listUnsubscribeHeader,
10497
10680
  loadLatestAdmissionRevision,
10498
10681
  loadOfferableTiers,
10499
10682
  loadTiers,
@@ -10557,6 +10740,7 @@ export {
10557
10740
  refundNetworkMembership,
10558
10741
  refundedPatch,
10559
10742
  render,
10743
+ renderHtmlBody,
10560
10744
  renderSignupControlThemeHead,
10561
10745
  renderSummary,
10562
10746
  renderTemplateBody,
@@ -10611,6 +10795,7 @@ export {
10611
10795
  submitApplication,
10612
10796
  subscriptionIdempotencyKey,
10613
10797
  syncApplicationToCrm,
10798
+ textToHtml,
10614
10799
  tierIsFree,
10615
10800
  tierPayable,
10616
10801
  updateClerkUserMetadata,