@odla-ai/chapter 0.60.0 → 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 +14 -0
- package/dist/{chunk-J7ES2OWN.js → chunk-24L7FANC.js} +3 -2
- package/dist/{chunk-J7ES2OWN.js.map → chunk-24L7FANC.js.map} +1 -1
- package/dist/{chunk-QNP3UEIL.js → chunk-PKVHXKGJ.js} +3 -1
- package/dist/chunk-PKVHXKGJ.js.map +1 -0
- package/dist/{chunk-D5UR5H6R.js → chunk-RTJKUOFE.js} +2 -2
- package/dist/{copy-context-BP3G1iBd.d.ts → copy-context-nsw2DwSK.d.ts} +1 -1
- package/dist/index.cjs +112 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +88 -41
- 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 +160 -115
- 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 +89 -44
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-QNP3UEIL.js.map +0 -1
- /package/dist/{chunk-D5UR5H6R.js.map → chunk-RTJKUOFE.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1404,6 +1404,8 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1404
1404
|
enabled: "Enabled",
|
|
1405
1405
|
subject: "Subject",
|
|
1406
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.",
|
|
1407
1409
|
save: "Save changes",
|
|
1408
1410
|
sendLog: "Send log",
|
|
1409
1411
|
sentColumn: "Sent",
|
|
@@ -7337,18 +7339,72 @@ function isRetiredEmailCopy(subject, text8) {
|
|
|
7337
7339
|
// src/email.ts
|
|
7338
7340
|
import { validateEmailCopy } from "@odla-ai/crm";
|
|
7339
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
|
+
|
|
7340
7396
|
// src/email-events.ts
|
|
7341
7397
|
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
7342
7398
|
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
7343
|
-
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
7344
|
-
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
7399
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
7400
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
7345
7401
|
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
7346
7402
|
var CHAPTER_EMAIL_EVENTS = {
|
|
7347
7403
|
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
7348
7404
|
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
7349
7405
|
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
7350
7406
|
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
7351
|
-
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
7407
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
|
|
7352
7408
|
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
7353
7409
|
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
7354
7410
|
};
|
|
@@ -7363,9 +7419,6 @@ function chapterEmailEvents(chapter) {
|
|
|
7363
7419
|
}
|
|
7364
7420
|
|
|
7365
7421
|
// src/email.ts
|
|
7366
|
-
function render(template, vars) {
|
|
7367
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
7368
|
-
}
|
|
7369
7422
|
function groupVars(group, vars) {
|
|
7370
7423
|
return {
|
|
7371
7424
|
...vars,
|
|
@@ -7398,7 +7451,9 @@ function planDelivery(input) {
|
|
|
7398
7451
|
values: vars,
|
|
7399
7452
|
tierId: vars.tierId
|
|
7400
7453
|
});
|
|
7401
|
-
|
|
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) };
|
|
7402
7457
|
const isProd = input.envName === "prod";
|
|
7403
7458
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
7404
7459
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
@@ -7411,11 +7466,13 @@ function planDelivery(input) {
|
|
|
7411
7466
|
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
7412
7467
|
|
|
7413
7468
|
` + renderedText : renderedText;
|
|
7414
|
-
|
|
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 };
|
|
7415
7472
|
}
|
|
7416
7473
|
|
|
7417
7474
|
// src/email-readiness.ts
|
|
7418
|
-
import { isEmailTierName, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
|
|
7475
|
+
import { isEmailTierName, templateSectionNames as templateSectionNames2, templateVars as templateVars2, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
|
|
7419
7476
|
|
|
7420
7477
|
// src/email-spam-signals.ts
|
|
7421
7478
|
var TRIGGER_PHRASES = [
|
|
@@ -7445,13 +7502,14 @@ var TRIGGER_PHRASES = [
|
|
|
7445
7502
|
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
7446
7503
|
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
7447
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;
|
|
7448
|
-
var
|
|
7505
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+/g;
|
|
7449
7506
|
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
7450
7507
|
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
7451
7508
|
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
7452
7509
|
function emailSpamSignals(copy) {
|
|
7453
7510
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
7454
7511
|
const text8 = typeof copy.text === "string" ? copy.text : "";
|
|
7512
|
+
const html = typeof copy.html === "string" ? copy.html : "";
|
|
7455
7513
|
const out = [];
|
|
7456
7514
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
7457
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, "")));
|
|
@@ -7464,7 +7522,8 @@ function emailSpamSignals(copy) {
|
|
|
7464
7522
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
7465
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." });
|
|
7466
7524
|
if (/!{2,}/.test(text8)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
7467
|
-
const urls = [
|
|
7525
|
+
const urls = [...`${text8}
|
|
7526
|
+
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
7468
7527
|
const hosts = urls.map((url) => {
|
|
7469
7528
|
try {
|
|
7470
7529
|
return new URL(url).hostname;
|
|
@@ -7509,10 +7568,14 @@ function validateEmailReadiness(input) {
|
|
|
7509
7568
|
const template = input.templates[key3];
|
|
7510
7569
|
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy2(template, { allowedVariables: event2.variables });
|
|
7511
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 }));
|
|
7512
7573
|
if (template) {
|
|
7513
|
-
const
|
|
7514
|
-
${template.text ?? ""}
|
|
7515
|
-
|
|
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)) {
|
|
7516
7579
|
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
7517
7580
|
}
|
|
7518
7581
|
}
|
|
@@ -7529,31 +7592,6 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
|
7529
7592
|
return { checkedAt: input.now ?? Date.now(), events: events2, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
7530
7593
|
}
|
|
7531
7594
|
|
|
7532
|
-
// src/email-html.ts
|
|
7533
|
-
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
7534
|
-
function escapeHtml(s) {
|
|
7535
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7536
|
-
}
|
|
7537
|
-
function lineHtml(line) {
|
|
7538
|
-
let out = "", last = 0;
|
|
7539
|
-
for (const match of line.matchAll(URL_RE2)) {
|
|
7540
|
-
out += escapeHtml(line.slice(last, match.index));
|
|
7541
|
-
const url = escapeHtml(match[0]);
|
|
7542
|
-
out += `<a href="${url}">${url}</a>`;
|
|
7543
|
-
last = match.index + match[0].length;
|
|
7544
|
-
}
|
|
7545
|
-
return out + escapeHtml(line.slice(last));
|
|
7546
|
-
}
|
|
7547
|
-
function textToHtml(text8) {
|
|
7548
|
-
const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
7549
|
-
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
7550
|
-
return `<!doctype html>
|
|
7551
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
7552
|
-
<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">
|
|
7553
|
-
${body}
|
|
7554
|
-
</body></html>`;
|
|
7555
|
-
}
|
|
7556
|
-
|
|
7557
7595
|
// src/email-deliverability.ts
|
|
7558
7596
|
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
7559
7597
|
function isRecipientFacing(event2) {
|
|
@@ -7625,10 +7663,14 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
7625
7663
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
7626
7664
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
7627
7665
|
}
|
|
7666
|
+
const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
|
|
7667
|
+
const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
|
|
7628
7668
|
return {
|
|
7629
7669
|
paymentAmount: formatted,
|
|
7630
7670
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
7631
|
-
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
|
|
7632
7674
|
};
|
|
7633
7675
|
}
|
|
7634
7676
|
async function catalogTierName(db, group, app) {
|
|
@@ -7736,7 +7778,7 @@ async function sendTemplated(deps, input) {
|
|
|
7736
7778
|
let messageId;
|
|
7737
7779
|
try {
|
|
7738
7780
|
const text8 = String(row2.body);
|
|
7739
|
-
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text8);
|
|
7781
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text8);
|
|
7740
7782
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
7741
7783
|
const response = await deps.sender.send({
|
|
7742
7784
|
from: fromAddress(deps.from, input.group.fromName),
|
|
@@ -10518,6 +10560,7 @@ export {
|
|
|
10518
10560
|
DEFAULT_SHARE_FIELDS,
|
|
10519
10561
|
EDGE_UNPAIRED,
|
|
10520
10562
|
GIFT_OFFER_CATALOG_VERSION,
|
|
10563
|
+
HTML_BODY_LIMIT,
|
|
10521
10564
|
JOURNEY_ENDINGS,
|
|
10522
10565
|
JOURNEY_STAGES,
|
|
10523
10566
|
MAX_GIFT_OFFERS,
|
|
@@ -10570,6 +10613,7 @@ export {
|
|
|
10570
10613
|
chapterDb,
|
|
10571
10614
|
chapterEmailEvents,
|
|
10572
10615
|
chapterFlowFieldContract,
|
|
10616
|
+
checkoutVariables,
|
|
10573
10617
|
clampArray,
|
|
10574
10618
|
classifyMemberStripeEvent,
|
|
10575
10619
|
classifyMemberSystemWrite,
|
|
@@ -10613,9 +10657,11 @@ export {
|
|
|
10613
10657
|
formatChapterCopy,
|
|
10614
10658
|
formationFields,
|
|
10615
10659
|
formationFlowFields,
|
|
10660
|
+
frozenCheckoutVariables,
|
|
10616
10661
|
getVaultSecret,
|
|
10617
10662
|
giftOfferActive,
|
|
10618
10663
|
hasDisclaimerAck,
|
|
10664
|
+
htmlBodyIssues,
|
|
10619
10665
|
hydrateSignupControlTheme,
|
|
10620
10666
|
injectSignupControlThemeHead,
|
|
10621
10667
|
introIdempotencyKey,
|
|
@@ -10694,6 +10740,7 @@ export {
|
|
|
10694
10740
|
refundNetworkMembership,
|
|
10695
10741
|
refundedPatch,
|
|
10696
10742
|
render,
|
|
10743
|
+
renderHtmlBody,
|
|
10697
10744
|
renderSignupControlThemeHead,
|
|
10698
10745
|
renderSummary,
|
|
10699
10746
|
renderTemplateBody,
|