@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/README.md +31 -0
- package/dist/{chunk-P6X3W2XP.js → chunk-24L7FANC.js} +13 -2
- package/dist/{chunk-P6X3W2XP.js.map → chunk-24L7FANC.js.map} +1 -1
- package/dist/{chunk-GADLTNJF.js → chunk-PKVHXKGJ.js} +4 -1
- package/dist/chunk-PKVHXKGJ.js.map +1 -0
- package/dist/{chunk-SDGVPIXF.js → chunk-RTJKUOFE.js} +2 -2
- package/dist/{copy-context-BuJBQafl.d.ts → copy-context-nsw2DwSK.d.ts} +1 -1
- package/dist/index.cjs +225 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -7
- package/dist/index.d.ts +102 -7
- package/dist/index.js +201 -16
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +275 -92
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +205 -22
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-GADLTNJF.js.map +0 -1
- /package/dist/{chunk-SDGVPIXF.js.map → chunk-RTJKUOFE.js.map} +0 -0
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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
|
-
|
|
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(/".*$/, ""));
|
|
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
|
|
7435
|
-
${template.text ?? ""}
|
|
7436
|
-
|
|
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 {
|
|
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:
|
|
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,
|