@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/worker/index.cjs
CHANGED
|
@@ -1707,20 +1707,74 @@ function isRetiredEmailCopy(subject, text10) {
|
|
|
1707
1707
|
}
|
|
1708
1708
|
|
|
1709
1709
|
// src/email.ts
|
|
1710
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
1711
|
+
|
|
1712
|
+
// src/email-render.ts
|
|
1710
1713
|
var import_crm5 = require("@odla-ai/crm");
|
|
1711
1714
|
|
|
1715
|
+
// src/email-html.ts
|
|
1716
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1717
|
+
function escapeHtml(s) {
|
|
1718
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1719
|
+
}
|
|
1720
|
+
function lineHtml(line) {
|
|
1721
|
+
let out = "", last = 0;
|
|
1722
|
+
for (const match of line.matchAll(URL_RE)) {
|
|
1723
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
1724
|
+
const url = escapeHtml(match[0]);
|
|
1725
|
+
out += `<a href="${url}">${url}</a>`;
|
|
1726
|
+
last = match.index + match[0].length;
|
|
1727
|
+
}
|
|
1728
|
+
return out + escapeHtml(line.slice(last));
|
|
1729
|
+
}
|
|
1730
|
+
function textToHtml(text10) {
|
|
1731
|
+
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1732
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1733
|
+
return `<!doctype html>
|
|
1734
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1735
|
+
<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">
|
|
1736
|
+
${body}
|
|
1737
|
+
</body></html>`;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/email-render.ts
|
|
1741
|
+
function render(template, vars) {
|
|
1742
|
+
return (0, import_crm5.renderTemplate)(template, vars);
|
|
1743
|
+
}
|
|
1744
|
+
var HTML_BODY_LIMIT = 2e5;
|
|
1745
|
+
function renderHtmlBody(template, vars) {
|
|
1746
|
+
const escaped = Object.fromEntries(Object.entries(vars).map(([name, value]) => [name, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
|
|
1747
|
+
return (0, import_crm5.renderTemplate)(template, escaped);
|
|
1748
|
+
}
|
|
1749
|
+
function htmlBodyIssues(html, options) {
|
|
1750
|
+
const issues = [];
|
|
1751
|
+
if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
|
|
1752
|
+
for (const message of (0, import_crm5.templateSectionIssues)(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
|
|
1753
|
+
const unknown = new Set((0, import_crm5.templateVars)(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
|
|
1754
|
+
for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
|
|
1755
|
+
if (options.values) {
|
|
1756
|
+
for (const name of (0, import_crm5.templateSectionNames)(html)) if (typeof options.values[name] !== "string") issues.push({ code: "value-missing", variable: name, message: `Missing event value: ${name} (HTML body).` });
|
|
1757
|
+
for (const variable of (0, import_crm5.templateLiveVars)(html, options.values)) {
|
|
1758
|
+
if (unknown.has(variable)) continue;
|
|
1759
|
+
const value = options.values[variable];
|
|
1760
|
+
if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return issues;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1712
1766
|
// src/email-events.ts
|
|
1713
1767
|
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
1714
1768
|
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
1715
|
-
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
1716
|
-
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
1769
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
1770
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
1717
1771
|
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
1718
1772
|
var CHAPTER_EMAIL_EVENTS = {
|
|
1719
1773
|
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
1720
1774
|
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
1721
1775
|
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
1722
1776
|
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
1723
|
-
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
1777
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
|
|
1724
1778
|
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
1725
1779
|
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
1726
1780
|
};
|
|
@@ -1735,9 +1789,6 @@ function chapterEmailEvents(chapter) {
|
|
|
1735
1789
|
}
|
|
1736
1790
|
|
|
1737
1791
|
// src/email.ts
|
|
1738
|
-
function render(template, vars) {
|
|
1739
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
1740
|
-
}
|
|
1741
1792
|
function groupVars(group, vars) {
|
|
1742
1793
|
return {
|
|
1743
1794
|
...vars,
|
|
@@ -1763,14 +1814,16 @@ function planDelivery(input) {
|
|
|
1763
1814
|
const vars = groupVars(input.group, input.vars);
|
|
1764
1815
|
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
1765
1816
|
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
1766
|
-
const issues = (0,
|
|
1817
|
+
const issues = (0, import_crm6.validateEmailCopy)(tpl, {
|
|
1767
1818
|
allowedVariables: event2?.variables,
|
|
1768
1819
|
optionalVariables: event2?.optionalVariables,
|
|
1769
1820
|
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
1770
1821
|
values: vars,
|
|
1771
1822
|
tierId: vars.tierId
|
|
1772
1823
|
});
|
|
1773
|
-
|
|
1824
|
+
const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
|
|
1825
|
+
const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
|
|
1826
|
+
if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
|
|
1774
1827
|
const isProd = input.envName === "prod";
|
|
1775
1828
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
1776
1829
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
@@ -1778,19 +1831,37 @@ function planDelivery(input) {
|
|
|
1778
1831
|
const renderedSubject = render(tpl.subject, vars);
|
|
1779
1832
|
const renderedText = render(tpl.text, vars);
|
|
1780
1833
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1781
|
-
if ((0,
|
|
1834
|
+
if ((0, import_crm6.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1782
1835
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1783
1836
|
const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1784
1837
|
|
|
1785
1838
|
` + renderedText : renderedText;
|
|
1786
|
-
|
|
1839
|
+
const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
|
|
1840
|
+
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;
|
|
1841
|
+
return { deliver: true, transport, to, subject, text: text10, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/email-deliverability.ts
|
|
1845
|
+
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
1846
|
+
function isRecipientFacing(event2) {
|
|
1847
|
+
return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
|
|
1848
|
+
}
|
|
1849
|
+
function listUnsubscribeHeader(replyTo) {
|
|
1850
|
+
const address = replyTo.trim();
|
|
1851
|
+
if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
|
|
1852
|
+
return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
|
|
1853
|
+
}
|
|
1854
|
+
function deliverabilityHeaders(input) {
|
|
1855
|
+
if (!isRecipientFacing(input.event)) return void 0;
|
|
1856
|
+
const value = listUnsubscribeHeader(input.replyTo);
|
|
1857
|
+
return value ? { "List-Unsubscribe": value } : void 0;
|
|
1787
1858
|
}
|
|
1788
1859
|
|
|
1789
1860
|
// src/notify.ts
|
|
1790
|
-
var
|
|
1861
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
1791
1862
|
|
|
1792
1863
|
// src/email-event-values.ts
|
|
1793
|
-
var
|
|
1864
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
1794
1865
|
|
|
1795
1866
|
// src/tiers.ts
|
|
1796
1867
|
function tierIsFree(tier) {
|
|
@@ -1899,10 +1970,14 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1899
1970
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
1900
1971
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
1901
1972
|
}
|
|
1973
|
+
const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
|
|
1974
|
+
const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
|
|
1902
1975
|
return {
|
|
1903
1976
|
paymentAmount: formatted,
|
|
1904
1977
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
1905
|
-
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
1978
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
|
|
1979
|
+
namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
|
|
1980
|
+
namedSeatRecipientName
|
|
1906
1981
|
};
|
|
1907
1982
|
}
|
|
1908
1983
|
async function catalogTierName(db, group, app) {
|
|
@@ -1960,6 +2035,8 @@ function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
|
1960
2035
|
purchaserName: "Sample Purchaser",
|
|
1961
2036
|
recipientName: "Sample Recipient",
|
|
1962
2037
|
chapterName: "Sample Chapter",
|
|
2038
|
+
namedSeat: "paid",
|
|
2039
|
+
namedSeatRecipientName: "Sample Recipient",
|
|
1963
2040
|
acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
|
|
1964
2041
|
joinUrl: `${origin}/join?seat=sample`
|
|
1965
2042
|
};
|
|
@@ -1977,7 +2054,7 @@ async function emailTierVariables(db, input) {
|
|
|
1977
2054
|
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
1978
2055
|
if (!tierId) return { tierName: "", tier: "" };
|
|
1979
2056
|
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
1980
|
-
const tierName2 = (0,
|
|
2057
|
+
const tierName2 = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
|
|
1981
2058
|
return { tierName: tierName2, tier: tierName2, tierId };
|
|
1982
2059
|
} catch {
|
|
1983
2060
|
return { tierName: "", tier: "" };
|
|
@@ -2037,7 +2114,7 @@ async function sendTemplated(deps, input) {
|
|
|
2037
2114
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
2038
2115
|
};
|
|
2039
2116
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
2040
|
-
if ((0,
|
|
2117
|
+
if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
2041
2118
|
try {
|
|
2042
2119
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
2043
2120
|
mutationId: `email-claim:${id7}:${attempt}`,
|
|
@@ -2057,11 +2134,16 @@ async function sendTemplated(deps, input) {
|
|
|
2057
2134
|
if (decision.transport === "log-only") return { sent: true };
|
|
2058
2135
|
let messageId;
|
|
2059
2136
|
try {
|
|
2137
|
+
const text10 = String(row2.body);
|
|
2138
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text10);
|
|
2139
|
+
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
2060
2140
|
const response = await deps.sender.send({
|
|
2061
2141
|
from: fromAddress(deps.from, input.group.fromName),
|
|
2062
2142
|
to: [decision.to],
|
|
2063
2143
|
subject: String(row2.subject),
|
|
2064
|
-
text:
|
|
2144
|
+
text: text10,
|
|
2145
|
+
...html ? { html } : {},
|
|
2146
|
+
...headers ? { headers } : {},
|
|
2065
2147
|
replyTo: input.group.replyTo
|
|
2066
2148
|
});
|
|
2067
2149
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
@@ -5957,7 +6039,7 @@ function belongsToSignup(account, applicationId) {
|
|
|
5957
6039
|
}
|
|
5958
6040
|
|
|
5959
6041
|
// src/member.ts
|
|
5960
|
-
var
|
|
6042
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
5961
6043
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
5962
6044
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
5963
6045
|
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -5991,7 +6073,7 @@ function resolveApplication(a) {
|
|
|
5991
6073
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
5992
6074
|
throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
|
|
5993
6075
|
}
|
|
5994
|
-
(0,
|
|
6076
|
+
(0, import_crm9.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
|
|
5995
6077
|
}
|
|
5996
6078
|
}
|
|
5997
6079
|
return {
|
|
@@ -6034,7 +6116,7 @@ function applicantProfile(chapter, fields) {
|
|
|
6034
6116
|
}
|
|
6035
6117
|
async function submitApplication(db, chapter, fields, opts) {
|
|
6036
6118
|
const app = chapter.application;
|
|
6037
|
-
const fieldStates = (0,
|
|
6119
|
+
const fieldStates = (0, import_crm9.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
6038
6120
|
for (const [f, state] of Object.entries(fieldStates)) {
|
|
6039
6121
|
if (!state.required) continue;
|
|
6040
6122
|
const v = fields[f];
|
|
@@ -6175,7 +6257,7 @@ async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
|
6175
6257
|
}
|
|
6176
6258
|
|
|
6177
6259
|
// src/worker-routes.ts
|
|
6178
|
-
var
|
|
6260
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
6179
6261
|
|
|
6180
6262
|
// src/discussion-reference-security.ts
|
|
6181
6263
|
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
@@ -7776,7 +7858,7 @@ var handleCrm = async (req, url, env, ctx) => {
|
|
|
7776
7858
|
const crmBase = ctx.crmBase;
|
|
7777
7859
|
if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + "/")) return null;
|
|
7778
7860
|
const db = ctx.makeDb(env);
|
|
7779
|
-
const routes = (0,
|
|
7861
|
+
const routes = (0, import_crm10.createCrmRoutes)({
|
|
7780
7862
|
crm: ctx.chapter.crm,
|
|
7781
7863
|
db,
|
|
7782
7864
|
authorize: async (r) => {
|
|
@@ -9187,7 +9269,7 @@ async function networkWriteAllowed(req, db, user, ctx, sensitiveRead = false) {
|
|
|
9187
9269
|
|
|
9188
9270
|
// src/network-person-reference.ts
|
|
9189
9271
|
var import_db6 = require("@odla-ai/db");
|
|
9190
|
-
var
|
|
9272
|
+
var import_crm12 = require("@odla-ai/crm");
|
|
9191
9273
|
|
|
9192
9274
|
// src/worker-network-fetch.ts
|
|
9193
9275
|
var NetworkTargetTransportError = class extends Error {
|
|
@@ -9223,7 +9305,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
9223
9305
|
}
|
|
9224
9306
|
|
|
9225
9307
|
// src/worker-network-admission-state.ts
|
|
9226
|
-
var
|
|
9308
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
9227
9309
|
var MAX_ASSIGNMENTS = 2e3;
|
|
9228
9310
|
var admissionRows = (value) => Array.isArray(value) ? value : [];
|
|
9229
9311
|
function assignmentId(targetId, personId) {
|
|
@@ -9250,7 +9332,7 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
9250
9332
|
return isValidEmail(normalized) ? normalized : null;
|
|
9251
9333
|
}
|
|
9252
9334
|
async function grantInput(db, ctx, assignment, resolveEmail) {
|
|
9253
|
-
const person = resolveEmail ? null : await (0,
|
|
9335
|
+
const person = resolveEmail ? null : await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
|
|
9254
9336
|
const email = resolveEmail ? await resolveEmail(assignment.personId) : person ? personAdmissionEmail(ctx, person) : null;
|
|
9255
9337
|
if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
|
|
9256
9338
|
return {
|
|
@@ -9456,7 +9538,7 @@ function chapterPersonReference(targetId, recordId) {
|
|
|
9456
9538
|
}
|
|
9457
9539
|
async function resolveAdmissionPersonEmail(db, env, ctx, target, personId) {
|
|
9458
9540
|
if (!personId.startsWith("chapter-person:")) {
|
|
9459
|
-
const person2 = await (0,
|
|
9541
|
+
const person2 = await (0, import_crm12.getRecord)({ crm: ctx.chapter.crm, db }, personId);
|
|
9460
9542
|
return person2 ? personAdmissionEmail(ctx, person2) : null;
|
|
9461
9543
|
}
|
|
9462
9544
|
const prefix = `chapter-person:${target.id}:`;
|
|
@@ -11355,10 +11437,10 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
11355
11437
|
};
|
|
11356
11438
|
|
|
11357
11439
|
// src/worker-routes-people-profile.ts
|
|
11358
|
-
var
|
|
11440
|
+
var import_crm14 = require("@odla-ai/crm");
|
|
11359
11441
|
|
|
11360
11442
|
// src/people-profile.ts
|
|
11361
|
-
var
|
|
11443
|
+
var import_crm13 = require("@odla-ai/crm");
|
|
11362
11444
|
var sameValue = (a, b) => (a == null || a === "") && (b == null || b === "") || JSON.stringify(a) === JSON.stringify(b);
|
|
11363
11445
|
function profileReadOnlyFields(record4, ctx) {
|
|
11364
11446
|
return [
|
|
@@ -11373,17 +11455,17 @@ async function updateChapterPerson(opts) {
|
|
|
11373
11455
|
const { db, env, ctx, record: record4 } = opts;
|
|
11374
11456
|
const input = Object.fromEntries(Object.entries(opts.input).filter(([key3, value]) => !sameValue(value, record4.fields[key3])));
|
|
11375
11457
|
const forbidden = profileReadOnlyFields(record4, ctx).filter((key3) => key3 in input);
|
|
11376
|
-
if (forbidden.length) throw new
|
|
11458
|
+
if (forbidden.length) throw new import_crm13.CrmInputError(
|
|
11377
11459
|
"Use the verified identity or application workflow for protected fields",
|
|
11378
11460
|
Object.fromEntries(forbidden.map((key3) => [key3, "This field is managed by its authoritative workflow"]))
|
|
11379
11461
|
);
|
|
11380
11462
|
const appId = record4.fields.applicationId;
|
|
11381
11463
|
const app = typeof appId === "string" && appId ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : null;
|
|
11382
11464
|
if (appId && (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || String(app.email).trim().toLowerCase() !== String(record4.primaryEmail).trim().toLowerCase())) {
|
|
11383
|
-
throw new
|
|
11465
|
+
throw new import_crm13.CrmInputError("The application identity must be repaired before editing this profile");
|
|
11384
11466
|
}
|
|
11385
11467
|
const prepared = ctx.chapter.crm.prepare(record4.type, input, record4);
|
|
11386
|
-
if (!prepared.ok) throw new
|
|
11468
|
+
if (!prepared.ok) throw new import_crm13.CrmInputError("invalid input", prepared.errors);
|
|
11387
11469
|
const appAttrs = {};
|
|
11388
11470
|
if (app) {
|
|
11389
11471
|
const mirrored = /* @__PURE__ */ new Set(["firstName", "lastName", "phone", "linkedin", ...ctx.chapter.application.crmFields]);
|
|
@@ -11412,7 +11494,7 @@ async function updateChapterPerson(opts) {
|
|
|
11412
11494
|
}] });
|
|
11413
11495
|
}
|
|
11414
11496
|
};
|
|
11415
|
-
return (0,
|
|
11497
|
+
return (0, import_crm13.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
|
|
11416
11498
|
id: record4.id,
|
|
11417
11499
|
input,
|
|
11418
11500
|
expectedUpdatedAt: opts.expectedRevision ?? record4.updatedAt,
|
|
@@ -11457,7 +11539,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
11457
11539
|
} catch {
|
|
11458
11540
|
return json({ error: "invalid record id" }, 422);
|
|
11459
11541
|
}
|
|
11460
|
-
const record4 = await (0,
|
|
11542
|
+
const record4 = await (0, import_crm14.getRecord)({ crm: ctx.chapter.crm, db }, id7);
|
|
11461
11543
|
if (!record4) return json({ error: "not found" }, 404);
|
|
11462
11544
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
11463
11545
|
if (match?.[2]) {
|
|
@@ -11509,7 +11591,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
11509
11591
|
});
|
|
11510
11592
|
return json(result);
|
|
11511
11593
|
} catch (error) {
|
|
11512
|
-
if (error instanceof
|
|
11594
|
+
if (error instanceof import_crm14.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
|
|
11513
11595
|
if (error.code === "transact_guard_failed") return json({ error: "profile changed; reload before saving" }, 409);
|
|
11514
11596
|
return json({ error: "profile update failed" }, 503);
|
|
11515
11597
|
}
|
|
@@ -11684,10 +11766,10 @@ async function repairAccountPeoplePage(deps, after) {
|
|
|
11684
11766
|
}
|
|
11685
11767
|
|
|
11686
11768
|
// src/crm-application-repair.ts
|
|
11687
|
-
var
|
|
11769
|
+
var import_crm15 = require("@odla-ai/crm");
|
|
11688
11770
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
11689
11771
|
const deps = { crm: chapter.crm, db };
|
|
11690
|
-
const record4 = await (0,
|
|
11772
|
+
const record4 = await (0, import_crm15.getRecord)(deps, recordId);
|
|
11691
11773
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
11692
11774
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
11693
11775
|
const fields = record4.fields;
|
|
@@ -11705,7 +11787,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
11705
11787
|
}
|
|
11706
11788
|
const app = [...applications.values()][0];
|
|
11707
11789
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
11708
|
-
await (0,
|
|
11790
|
+
await (0, import_crm15.updateRecord)(deps, {
|
|
11709
11791
|
id: recordId,
|
|
11710
11792
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
11711
11793
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -12526,10 +12608,90 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
12526
12608
|
};
|
|
12527
12609
|
|
|
12528
12610
|
// src/email-admin-config.ts
|
|
12611
|
+
var import_crm17 = require("@odla-ai/crm");
|
|
12612
|
+
|
|
12613
|
+
// src/email-readiness.ts
|
|
12529
12614
|
var import_crm16 = require("@odla-ai/crm");
|
|
12530
12615
|
|
|
12616
|
+
// src/email-spam-signals.ts
|
|
12617
|
+
var TRIGGER_PHRASES = [
|
|
12618
|
+
"act now",
|
|
12619
|
+
"click here",
|
|
12620
|
+
"limited time",
|
|
12621
|
+
"guaranteed",
|
|
12622
|
+
"risk-free",
|
|
12623
|
+
"risk free",
|
|
12624
|
+
"no obligation",
|
|
12625
|
+
"100% free",
|
|
12626
|
+
"winner",
|
|
12627
|
+
"congratulations",
|
|
12628
|
+
"urgent",
|
|
12629
|
+
"double your",
|
|
12630
|
+
"earn money",
|
|
12631
|
+
"make money",
|
|
12632
|
+
"cash bonus",
|
|
12633
|
+
"once in a lifetime",
|
|
12634
|
+
"exclusive deal",
|
|
12635
|
+
"order now",
|
|
12636
|
+
"buy now",
|
|
12637
|
+
"this is not spam",
|
|
12638
|
+
"dear friend",
|
|
12639
|
+
"dear customer"
|
|
12640
|
+
];
|
|
12641
|
+
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
12642
|
+
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
12643
|
+
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;
|
|
12644
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+/g;
|
|
12645
|
+
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
12646
|
+
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
12647
|
+
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
12648
|
+
function emailSpamSignals(copy) {
|
|
12649
|
+
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12650
|
+
const text10 = typeof copy.text === "string" ? copy.text : "";
|
|
12651
|
+
const html = typeof copy.html === "string" ? copy.html : "";
|
|
12652
|
+
const out = [];
|
|
12653
|
+
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
12654
|
+
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, "")));
|
|
12655
|
+
if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
|
|
12656
|
+
if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
|
|
12657
|
+
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." });
|
|
12658
|
+
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
12659
|
+
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
12660
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
|
|
12661
|
+
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12662
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text10))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
12663
|
+
if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
12664
|
+
const urls = [...`${text10}
|
|
12665
|
+
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
12666
|
+
const hosts = urls.map((url) => {
|
|
12667
|
+
try {
|
|
12668
|
+
return new URL(url).hostname;
|
|
12669
|
+
} catch {
|
|
12670
|
+
return "";
|
|
12671
|
+
}
|
|
12672
|
+
});
|
|
12673
|
+
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." });
|
|
12674
|
+
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." });
|
|
12675
|
+
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." });
|
|
12676
|
+
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." });
|
|
12677
|
+
if (urls.length && words(text10).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." });
|
|
12678
|
+
return out;
|
|
12679
|
+
}
|
|
12680
|
+
function emailSenderWarnings(input) {
|
|
12681
|
+
const out = [];
|
|
12682
|
+
const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
|
|
12683
|
+
const reply = (input.replyTo ?? "").trim().toLowerCase();
|
|
12684
|
+
const domain = (address) => address.split("@")[1] ?? "";
|
|
12685
|
+
if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
|
|
12686
|
+
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." });
|
|
12687
|
+
}
|
|
12688
|
+
if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
|
|
12689
|
+
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.` });
|
|
12690
|
+
}
|
|
12691
|
+
return out;
|
|
12692
|
+
}
|
|
12693
|
+
|
|
12531
12694
|
// src/email-readiness.ts
|
|
12532
|
-
var import_crm15 = require("@odla-ai/crm");
|
|
12533
12695
|
function validateEmailReadiness(input) {
|
|
12534
12696
|
const issues = [];
|
|
12535
12697
|
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
@@ -12537,26 +12699,36 @@ function validateEmailReadiness(input) {
|
|
|
12537
12699
|
}
|
|
12538
12700
|
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
12539
12701
|
for (const tier of input.tiers ?? []) {
|
|
12540
|
-
if (!(0,
|
|
12702
|
+
if (!(0, import_crm16.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
12541
12703
|
}
|
|
12542
|
-
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0,
|
|
12704
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm16.isEmailTierName)(tier.name, tier.id));
|
|
12543
12705
|
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
12544
12706
|
const events = Object.entries(input.events).map(([key3, event2]) => {
|
|
12545
12707
|
const template = input.templates[key3];
|
|
12546
|
-
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0,
|
|
12708
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm16.validateEmailCopy)(template, { allowedVariables: event2.variables });
|
|
12547
12709
|
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
12710
|
+
const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
|
|
12711
|
+
if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
|
|
12548
12712
|
if (template) {
|
|
12549
|
-
const
|
|
12550
|
-
${template.text ?? ""}
|
|
12551
|
-
|
|
12713
|
+
const copy = `${template.subject ?? ""}
|
|
12714
|
+
${template.text ?? ""}
|
|
12715
|
+
${html}`;
|
|
12716
|
+
const sectionOnly = new Set((0, import_crm16.templateSectionNames)(copy));
|
|
12717
|
+
for (const name of (0, import_crm16.templateVars)(copy)) if (name in input.groupValues && !input.groupValues[name]?.trim() && !sectionOnly.has(name)) {
|
|
12552
12718
|
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
12553
12719
|
}
|
|
12554
12720
|
}
|
|
12555
12721
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
12556
12722
|
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
12557
|
-
return {
|
|
12723
|
+
return {
|
|
12724
|
+
...event2,
|
|
12725
|
+
key: key3,
|
|
12726
|
+
status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
|
|
12727
|
+
issues: problems,
|
|
12728
|
+
warnings: template ? emailSpamSignals(template) : []
|
|
12729
|
+
};
|
|
12558
12730
|
});
|
|
12559
|
-
return { checkedAt: input.now ?? Date.now(), events, issues };
|
|
12731
|
+
return { checkedAt: input.now ?? Date.now(), events, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
12560
12732
|
}
|
|
12561
12733
|
|
|
12562
12734
|
// src/email-admin-config.ts
|
|
@@ -12564,13 +12736,14 @@ var str2 = (value) => typeof value === "string" ? value : "";
|
|
|
12564
12736
|
async function emailSettings(db, ctx, env) {
|
|
12565
12737
|
const hub = ctx.chapter.mode === "hub";
|
|
12566
12738
|
const crmDeps3 = { db, crm: ctx.chapter.crm };
|
|
12567
|
-
const group = hub ? await (0,
|
|
12739
|
+
const group = hub ? await (0, import_crm17.getConfig)(crmDeps3) : (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
12568
12740
|
const raw2 = group?.emailTemplates;
|
|
12569
12741
|
const stored = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
|
|
12570
12742
|
const events = chapterEmailEvents(ctx.chapter);
|
|
12571
12743
|
const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
|
|
12572
12744
|
subject: str2(stored[key3]?.subject),
|
|
12573
12745
|
text: str2(stored[key3]?.text),
|
|
12746
|
+
html: str2(stored[key3]?.html),
|
|
12574
12747
|
enabled: stored[key3]?.enabled
|
|
12575
12748
|
}]));
|
|
12576
12749
|
let tiers = [], tiersUnavailable = false;
|
|
@@ -12582,7 +12755,14 @@ async function emailSettings(db, ctx, env) {
|
|
|
12582
12755
|
}
|
|
12583
12756
|
}
|
|
12584
12757
|
const groupValues = Object.fromEntries(["refundPolicyText", "commitmentText", "normsText"].map((key3) => [key3, str2(group?.[key3])]));
|
|
12585
|
-
const validation = validateEmailReadiness({
|
|
12758
|
+
const validation = validateEmailReadiness({
|
|
12759
|
+
events,
|
|
12760
|
+
templates: stored,
|
|
12761
|
+
groupValues,
|
|
12762
|
+
tiers: hub ? void 0 : tiers,
|
|
12763
|
+
tiersUnavailable,
|
|
12764
|
+
sender: { fromEmail: env.EMAIL_FROM ?? null, replyTo: str2(group?.replyTo) }
|
|
12765
|
+
});
|
|
12586
12766
|
return { group, events, data: {
|
|
12587
12767
|
groupId: ctx.chapter.id,
|
|
12588
12768
|
name: ctx.chapter.name,
|
|
@@ -12610,12 +12790,14 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12610
12790
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12611
12791
|
const t = value;
|
|
12612
12792
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12613
|
-
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12793
|
+
const subject = str2(t.subject).trim(), text10 = str2(t.text), html = str2(t.html);
|
|
12794
|
+
if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
|
|
12795
|
+
const issues = (0, import_crm17.validateEmailCopy)({ subject, text: text10 }, { allowedVariables: events[key3].variables });
|
|
12796
|
+
const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
|
|
12797
|
+
if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
|
|
12798
|
+
return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
|
|
12617
12799
|
}
|
|
12618
|
-
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12800
|
+
clean[key3] = { subject, text: text10, ...html.trim() ? { html } : {}, enabled: t.enabled };
|
|
12619
12801
|
}
|
|
12620
12802
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12621
12803
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -12628,7 +12810,7 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12628
12810
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
12629
12811
|
if (ctx.chapter.mode === "hub") {
|
|
12630
12812
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
12631
|
-
await (0,
|
|
12813
|
+
await (0, import_crm17.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
12632
12814
|
} else {
|
|
12633
12815
|
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
12634
12816
|
await db.transact([
|
|
@@ -12636,11 +12818,12 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12636
12818
|
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
12637
12819
|
]);
|
|
12638
12820
|
}
|
|
12639
|
-
|
|
12821
|
+
const warnings = Object.fromEntries(Object.entries(clean).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
|
|
12822
|
+
return json({ ok: true, warnings });
|
|
12640
12823
|
}
|
|
12641
12824
|
|
|
12642
12825
|
// src/worker-routes-admin-comms.ts
|
|
12643
|
-
var
|
|
12826
|
+
var import_crm18 = require("@odla-ai/crm");
|
|
12644
12827
|
async function gate5(req, env, ctx) {
|
|
12645
12828
|
const rawDb = ctx.makeDb(env);
|
|
12646
12829
|
const u = await ctx.verifyUser(req, env);
|
|
@@ -12673,7 +12856,7 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
|
12673
12856
|
const got = await gate5(req, env, ctx);
|
|
12674
12857
|
if (got instanceof Response) return got;
|
|
12675
12858
|
if (ctx.chapter.mode === "hub") {
|
|
12676
|
-
const rows5 = await (0,
|
|
12859
|
+
const rows5 = await (0, import_crm18.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
|
|
12677
12860
|
return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
|
|
12678
12861
|
}
|
|
12679
12862
|
const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
|
|
@@ -12770,7 +12953,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
12770
12953
|
};
|
|
12771
12954
|
|
|
12772
12955
|
// src/worker-routes-network.ts
|
|
12773
|
-
var
|
|
12956
|
+
var import_crm20 = require("@odla-ai/crm");
|
|
12774
12957
|
var import_db9 = require("@odla-ai/db");
|
|
12775
12958
|
|
|
12776
12959
|
// src/network-edge.ts
|
|
@@ -12796,7 +12979,7 @@ function unpairedResponse(unpaired) {
|
|
|
12796
12979
|
}
|
|
12797
12980
|
|
|
12798
12981
|
// src/worker-network-push.ts
|
|
12799
|
-
var
|
|
12982
|
+
var import_crm19 = require("@odla-ai/crm");
|
|
12800
12983
|
var import_db8 = require("@odla-ai/db");
|
|
12801
12984
|
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
12802
12985
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12808,7 +12991,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12808
12991
|
};
|
|
12809
12992
|
if (!secret) {
|
|
12810
12993
|
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
12811
|
-
await (0,
|
|
12994
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12812
12995
|
recordId: record4.id,
|
|
12813
12996
|
targetId: target.id,
|
|
12814
12997
|
status: "failed",
|
|
@@ -12847,7 +13030,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12847
13030
|
const responseBody = await res.json().catch(() => ({}));
|
|
12848
13031
|
if (!res.ok) {
|
|
12849
13032
|
const error = responseBody.error ?? "follower rejected the record";
|
|
12850
|
-
await (0,
|
|
13033
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12851
13034
|
recordId: record4.id,
|
|
12852
13035
|
targetId: target.id,
|
|
12853
13036
|
status: "failed",
|
|
@@ -12862,7 +13045,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12862
13045
|
error
|
|
12863
13046
|
};
|
|
12864
13047
|
}
|
|
12865
|
-
await (0,
|
|
13048
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12866
13049
|
recordId: record4.id,
|
|
12867
13050
|
targetId: target.id,
|
|
12868
13051
|
status: "delivered",
|
|
@@ -12878,7 +13061,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12878
13061
|
};
|
|
12879
13062
|
} catch (err) {
|
|
12880
13063
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
12881
|
-
await (0,
|
|
13064
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12882
13065
|
recordId: record4.id,
|
|
12883
13066
|
targetId: target.id,
|
|
12884
13067
|
status: "failed",
|
|
@@ -12931,7 +13114,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
12931
13114
|
const verified = await (0, import_db9.verifyFederatedRequest)(req, { secret });
|
|
12932
13115
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
12933
13116
|
const types = await Promise.all(
|
|
12934
|
-
Object.keys(ctx.chapter.crm.config.types).map((type) => (0,
|
|
13117
|
+
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm20.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
12935
13118
|
);
|
|
12936
13119
|
const activeConfiguration = ctx.chapter.signupControl ? await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV) : null;
|
|
12937
13120
|
const snapshot = {
|
|
@@ -13078,7 +13261,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
13078
13261
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
13079
13262
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
13080
13263
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
13081
|
-
const record4 = await (0,
|
|
13264
|
+
const record4 = await (0, import_crm20.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
13082
13265
|
if (!record4) return json({ error: "record not found" }, 404);
|
|
13083
13266
|
const results = await Promise.all(
|
|
13084
13267
|
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
@@ -13087,11 +13270,11 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
13087
13270
|
};
|
|
13088
13271
|
|
|
13089
13272
|
// src/worker-routes-network-records-follower.ts
|
|
13090
|
-
var
|
|
13273
|
+
var import_crm24 = require("@odla-ai/crm");
|
|
13091
13274
|
var import_db10 = require("@odla-ai/db");
|
|
13092
13275
|
|
|
13093
13276
|
// src/network-application-action.ts
|
|
13094
|
-
var
|
|
13277
|
+
var import_crm21 = require("@odla-ai/crm");
|
|
13095
13278
|
var ACTION_META = "chapterNetworkMutation";
|
|
13096
13279
|
async function networkApplicationAction(opts) {
|
|
13097
13280
|
const { db, env, ctx, record: record4, input, sourceId, activityId, fingerprint } = opts;
|
|
@@ -13181,7 +13364,7 @@ async function networkApplicationAction(opts) {
|
|
|
13181
13364
|
const current = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
|
|
13182
13365
|
if (!current || current.status !== input.stage) return json({ error: "application reconciliation pending", code: "action_pending" }, 503);
|
|
13183
13366
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) });
|
|
13184
|
-
const projected = await (0,
|
|
13367
|
+
const projected = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
13185
13368
|
if (!projected || projected.stage !== input.stage) return json({ error: "CRM reconciliation pending", code: "action_pending" }, 503);
|
|
13186
13369
|
const audit = (await db.query({ crm_activity: { $: { where: { id: activityId }, limit: 1 } } })).crm_activity?.[0];
|
|
13187
13370
|
const priorMeta = audit?.meta;
|
|
@@ -13191,7 +13374,7 @@ async function networkApplicationAction(opts) {
|
|
|
13191
13374
|
} },
|
|
13192
13375
|
{ t: "update", ns: "crm_activity", id: lockId, attrs: { meta: { state: "complete", activityId } } }
|
|
13193
13376
|
], { guards: [{ ns: "crm_activity", id: lockId, exists: true, equals: { meta: { state: "pending", activityId } } }] });
|
|
13194
|
-
const updated = await (0,
|
|
13377
|
+
const updated = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
13195
13378
|
if (!updated) return json({ error: "record not found" }, 404);
|
|
13196
13379
|
return { record: updated, duplicate: Boolean(receipt) };
|
|
13197
13380
|
}
|
|
@@ -13227,7 +13410,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
13227
13410
|
}
|
|
13228
13411
|
|
|
13229
13412
|
// src/worker-routes-network-records-search.ts
|
|
13230
|
-
var
|
|
13413
|
+
var import_crm22 = require("@odla-ai/crm");
|
|
13231
13414
|
var SEARCH_BATCH_SIZE = 1e3;
|
|
13232
13415
|
var MAX_READER_SEARCH_POPULATION = 1e4;
|
|
13233
13416
|
var ReaderSearchPopulationError = class extends Error {
|
|
@@ -13268,19 +13451,19 @@ async function listVisibleSearchPopulation(db, where, window, count) {
|
|
|
13268
13451
|
(_, index2) => index2 * SEARCH_BATCH_SIZE
|
|
13269
13452
|
);
|
|
13270
13453
|
const pages = await Promise.all(offsets.map((offset) => db.query({
|
|
13271
|
-
[
|
|
13454
|
+
[import_crm22.CRM_NS.record]: { $: {
|
|
13272
13455
|
where,
|
|
13273
13456
|
order: { [window.sort.field]: window.sort.dir },
|
|
13274
13457
|
limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
|
|
13275
13458
|
offset
|
|
13276
13459
|
} }
|
|
13277
13460
|
})));
|
|
13278
|
-
return pages.flatMap((page) => (page[
|
|
13461
|
+
return pages.flatMap((page) => (page[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord));
|
|
13279
13462
|
}
|
|
13280
13463
|
async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
13281
13464
|
const where = baseWhere(type, window.stage);
|
|
13282
13465
|
const crmDb = db;
|
|
13283
|
-
const aggregate = await crmDb.aggregate(
|
|
13466
|
+
const aggregate = await crmDb.aggregate(import_crm22.CRM_NS.record, { count: true }, { where });
|
|
13284
13467
|
const candidateCount = aggregate.count ?? 0;
|
|
13285
13468
|
if (window.search) {
|
|
13286
13469
|
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
@@ -13292,7 +13475,7 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
13292
13475
|
};
|
|
13293
13476
|
}
|
|
13294
13477
|
const result = await crmDb.query({
|
|
13295
|
-
[
|
|
13478
|
+
[import_crm22.CRM_NS.record]: { $: {
|
|
13296
13479
|
where,
|
|
13297
13480
|
order: { [window.sort.field]: window.sort.dir },
|
|
13298
13481
|
limit: window.limit,
|
|
@@ -13300,19 +13483,19 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
13300
13483
|
} }
|
|
13301
13484
|
});
|
|
13302
13485
|
return {
|
|
13303
|
-
records: (result[
|
|
13486
|
+
records: (result[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord),
|
|
13304
13487
|
total: candidateCount
|
|
13305
13488
|
};
|
|
13306
13489
|
}
|
|
13307
13490
|
|
|
13308
13491
|
// src/network-mutation-receipt.ts
|
|
13309
|
-
var
|
|
13492
|
+
var import_crm23 = require("@odla-ai/crm");
|
|
13310
13493
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
13311
13494
|
async function priorMutation(db, id7) {
|
|
13312
13495
|
const result = await db.query({
|
|
13313
|
-
[
|
|
13496
|
+
[import_crm23.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
13314
13497
|
});
|
|
13315
|
-
const row2 = (result[
|
|
13498
|
+
const row2 = (result[import_crm23.CRM_NS.activity] ?? [])[0];
|
|
13316
13499
|
const meta = row2?.meta;
|
|
13317
13500
|
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
13318
13501
|
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint, pending: receipt.state === "pending", failed: receipt.state === "failed" } : null;
|
|
@@ -13336,7 +13519,7 @@ function mutationError(error) {
|
|
|
13336
13519
|
if (error?.code === "transact_guard_failed") {
|
|
13337
13520
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
13338
13521
|
}
|
|
13339
|
-
if (error instanceof
|
|
13522
|
+
if (error instanceof import_crm24.CrmInputError) {
|
|
13340
13523
|
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
13341
13524
|
}
|
|
13342
13525
|
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
@@ -13391,7 +13574,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13391
13574
|
}
|
|
13392
13575
|
if (prior?.failed) return json({ error: "this operation was not applied; reload before choosing a new action", code: "action_failed" }, 409);
|
|
13393
13576
|
if (prior && !prior.pending) {
|
|
13394
|
-
const replayed = await (0,
|
|
13577
|
+
const replayed = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
13395
13578
|
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
13396
13579
|
const result = {
|
|
13397
13580
|
version: 1,
|
|
@@ -13401,7 +13584,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13401
13584
|
};
|
|
13402
13585
|
return json(result);
|
|
13403
13586
|
}
|
|
13404
|
-
const record4 = await (0,
|
|
13587
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
13405
13588
|
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
13406
13589
|
if (!prior?.pending && record4.updatedAt !== input.expectedRevision) {
|
|
13407
13590
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
@@ -13464,7 +13647,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13464
13647
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
13465
13648
|
});
|
|
13466
13649
|
}
|
|
13467
|
-
const updated = hasFields ? await (0,
|
|
13650
|
+
const updated = hasFields ? await (0, import_crm24.updateRecord)(deps, {
|
|
13468
13651
|
id: record4.id,
|
|
13469
13652
|
input: fields,
|
|
13470
13653
|
mutationId: activityId,
|
|
@@ -13475,7 +13658,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13475
13658
|
authorId: `network:${verified.sender}`,
|
|
13476
13659
|
meta: auditMeta
|
|
13477
13660
|
}
|
|
13478
|
-
}) : await (0,
|
|
13661
|
+
}) : await (0, import_crm24.setStage)(deps, {
|
|
13479
13662
|
id: record4.id,
|
|
13480
13663
|
to: input.stage,
|
|
13481
13664
|
authorId: `network:${verified.sender}`,
|
|
@@ -13502,7 +13685,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13502
13685
|
const recordId = url.searchParams.get("recordId");
|
|
13503
13686
|
if (recordId !== null) {
|
|
13504
13687
|
if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
|
|
13505
|
-
const record4 = await (0,
|
|
13688
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
13506
13689
|
if (!record4 || record4.type !== type || record4.archived) return json({ error: "record not found" }, 404);
|
|
13507
13690
|
return json({
|
|
13508
13691
|
version: 1,
|
|
@@ -13797,7 +13980,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
13797
13980
|
};
|
|
13798
13981
|
|
|
13799
13982
|
// src/worker-routes-network-shared-notes.ts
|
|
13800
|
-
var
|
|
13983
|
+
var import_crm25 = require("@odla-ai/crm");
|
|
13801
13984
|
var import_db12 = require("@odla-ai/db");
|
|
13802
13985
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
13803
13986
|
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
@@ -13828,7 +14011,7 @@ function sharedNote(activity, sourceId) {
|
|
|
13828
14011
|
}
|
|
13829
14012
|
async function listSharedNotes(db, sourceId, recordId) {
|
|
13830
14013
|
const result = await db.query({
|
|
13831
|
-
[
|
|
14014
|
+
[import_crm25.CRM_NS.activity]: {
|
|
13832
14015
|
$: {
|
|
13833
14016
|
where: {
|
|
13834
14017
|
and: [
|
|
@@ -13841,7 +14024,7 @@ async function listSharedNotes(db, sourceId, recordId) {
|
|
|
13841
14024
|
}
|
|
13842
14025
|
}
|
|
13843
14026
|
});
|
|
13844
|
-
return (result[
|
|
14027
|
+
return (result[import_crm25.CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
13845
14028
|
const note = sharedNote(activity, sourceId);
|
|
13846
14029
|
return note ? [note] : [];
|
|
13847
14030
|
});
|
|
@@ -13869,7 +14052,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
13869
14052
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
13870
14053
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
13871
14054
|
}
|
|
13872
|
-
const record4 = await (0,
|
|
14055
|
+
const record4 = await (0, import_crm25.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
13873
14056
|
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
13874
14057
|
if (req.method === "GET") {
|
|
13875
14058
|
return json({
|
|
@@ -13891,7 +14074,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
13891
14074
|
}
|
|
13892
14075
|
const createdAt = Date.now();
|
|
13893
14076
|
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
13894
|
-
const result = await (0,
|
|
14077
|
+
const result = await (0, import_crm25.addActivity)(
|
|
13895
14078
|
{
|
|
13896
14079
|
crm: ctx.chapter.crm,
|
|
13897
14080
|
db,
|
|
@@ -15520,7 +15703,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15520
15703
|
};
|
|
15521
15704
|
|
|
15522
15705
|
// src/worker-routes-formation.ts
|
|
15523
|
-
var
|
|
15706
|
+
var import_crm26 = require("@odla-ai/crm");
|
|
15524
15707
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
15525
15708
|
var handleFormation = async (req, url, env, ctx) => {
|
|
15526
15709
|
const formation = ctx.chapter.formation;
|
|
@@ -15572,7 +15755,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
15572
15755
|
const db = ctx.makeDb(env);
|
|
15573
15756
|
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
15574
15757
|
try {
|
|
15575
|
-
const created = await (0,
|
|
15758
|
+
const created = await (0, import_crm26.createRecord)(
|
|
15576
15759
|
{
|
|
15577
15760
|
crm: ctx.chapter.crm,
|
|
15578
15761
|
db,
|
|
@@ -17763,7 +17946,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17763
17946
|
}
|
|
17764
17947
|
|
|
17765
17948
|
// src/membership-chapter-delivery.ts
|
|
17766
|
-
var
|
|
17949
|
+
var import_crm27 = require("@odla-ai/crm");
|
|
17767
17950
|
|
|
17768
17951
|
// src/invitation-test-delivery.ts
|
|
17769
17952
|
function invitationDelivery(env, recipientEmail) {
|
|
@@ -17828,7 +18011,7 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
17828
18011
|
join.searchParams.set("seat", effect.seatId);
|
|
17829
18012
|
join.searchParams.set("tier", effect.skuId);
|
|
17830
18013
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
17831
|
-
if (template && !(0,
|
|
18014
|
+
if (template && !(0, import_crm27.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
|
|
17832
18015
|
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: {
|
|
17833
18016
|
...template,
|
|
17834
18017
|
text: "{{purchaserName}} has purchased a membership for you at {{chapterName}}.\n\n" + template.text
|