@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/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,37 +1831,14 @@ 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
|
-
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
// src/email-html.ts
|
|
1790
|
-
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1791
|
-
function escapeHtml(s) {
|
|
1792
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1793
|
-
}
|
|
1794
|
-
function lineHtml(line) {
|
|
1795
|
-
let out = "", last = 0;
|
|
1796
|
-
for (const match of line.matchAll(URL_RE)) {
|
|
1797
|
-
out += escapeHtml(line.slice(last, match.index));
|
|
1798
|
-
const url = escapeHtml(match[0]);
|
|
1799
|
-
out += `<a href="${url}">${url}</a>`;
|
|
1800
|
-
last = match.index + match[0].length;
|
|
1801
|
-
}
|
|
1802
|
-
return out + escapeHtml(line.slice(last));
|
|
1803
|
-
}
|
|
1804
|
-
function textToHtml(text10) {
|
|
1805
|
-
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1806
|
-
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1807
|
-
return `<!doctype html>
|
|
1808
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1809
|
-
<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">
|
|
1810
|
-
${body}
|
|
1811
|
-
</body></html>`;
|
|
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 };
|
|
1812
1842
|
}
|
|
1813
1843
|
|
|
1814
1844
|
// src/email-deliverability.ts
|
|
@@ -1828,10 +1858,10 @@ function deliverabilityHeaders(input) {
|
|
|
1828
1858
|
}
|
|
1829
1859
|
|
|
1830
1860
|
// src/notify.ts
|
|
1831
|
-
var
|
|
1861
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
1832
1862
|
|
|
1833
1863
|
// src/email-event-values.ts
|
|
1834
|
-
var
|
|
1864
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
1835
1865
|
|
|
1836
1866
|
// src/tiers.ts
|
|
1837
1867
|
function tierIsFree(tier) {
|
|
@@ -1940,10 +1970,14 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1940
1970
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
1941
1971
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
1942
1972
|
}
|
|
1973
|
+
const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
|
|
1974
|
+
const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
|
|
1943
1975
|
return {
|
|
1944
1976
|
paymentAmount: formatted,
|
|
1945
1977
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
1946
|
-
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
|
|
1947
1981
|
};
|
|
1948
1982
|
}
|
|
1949
1983
|
async function catalogTierName(db, group, app) {
|
|
@@ -2001,6 +2035,8 @@ function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
|
2001
2035
|
purchaserName: "Sample Purchaser",
|
|
2002
2036
|
recipientName: "Sample Recipient",
|
|
2003
2037
|
chapterName: "Sample Chapter",
|
|
2038
|
+
namedSeat: "paid",
|
|
2039
|
+
namedSeatRecipientName: "Sample Recipient",
|
|
2004
2040
|
acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
|
|
2005
2041
|
joinUrl: `${origin}/join?seat=sample`
|
|
2006
2042
|
};
|
|
@@ -2018,7 +2054,7 @@ async function emailTierVariables(db, input) {
|
|
|
2018
2054
|
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
2019
2055
|
if (!tierId) return { tierName: "", tier: "" };
|
|
2020
2056
|
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
2021
|
-
const tierName2 = (0,
|
|
2057
|
+
const tierName2 = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
|
|
2022
2058
|
return { tierName: tierName2, tier: tierName2, tierId };
|
|
2023
2059
|
} catch {
|
|
2024
2060
|
return { tierName: "", tier: "" };
|
|
@@ -2078,7 +2114,7 @@ async function sendTemplated(deps, input) {
|
|
|
2078
2114
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
2079
2115
|
};
|
|
2080
2116
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
2081
|
-
if ((0,
|
|
2117
|
+
if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
2082
2118
|
try {
|
|
2083
2119
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
2084
2120
|
mutationId: `email-claim:${id7}:${attempt}`,
|
|
@@ -2099,7 +2135,7 @@ async function sendTemplated(deps, input) {
|
|
|
2099
2135
|
let messageId;
|
|
2100
2136
|
try {
|
|
2101
2137
|
const text10 = String(row2.body);
|
|
2102
|
-
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text10);
|
|
2138
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text10);
|
|
2103
2139
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
2104
2140
|
const response = await deps.sender.send({
|
|
2105
2141
|
from: fromAddress(deps.from, input.group.fromName),
|
|
@@ -6003,7 +6039,7 @@ function belongsToSignup(account, applicationId) {
|
|
|
6003
6039
|
}
|
|
6004
6040
|
|
|
6005
6041
|
// src/member.ts
|
|
6006
|
-
var
|
|
6042
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
6007
6043
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
6008
6044
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
6009
6045
|
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -6037,7 +6073,7 @@ function resolveApplication(a) {
|
|
|
6037
6073
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
6038
6074
|
throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
|
|
6039
6075
|
}
|
|
6040
|
-
(0,
|
|
6076
|
+
(0, import_crm9.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
|
|
6041
6077
|
}
|
|
6042
6078
|
}
|
|
6043
6079
|
return {
|
|
@@ -6080,7 +6116,7 @@ function applicantProfile(chapter, fields) {
|
|
|
6080
6116
|
}
|
|
6081
6117
|
async function submitApplication(db, chapter, fields, opts) {
|
|
6082
6118
|
const app = chapter.application;
|
|
6083
|
-
const fieldStates = (0,
|
|
6119
|
+
const fieldStates = (0, import_crm9.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
6084
6120
|
for (const [f, state] of Object.entries(fieldStates)) {
|
|
6085
6121
|
if (!state.required) continue;
|
|
6086
6122
|
const v = fields[f];
|
|
@@ -6221,7 +6257,7 @@ async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
|
6221
6257
|
}
|
|
6222
6258
|
|
|
6223
6259
|
// src/worker-routes.ts
|
|
6224
|
-
var
|
|
6260
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
6225
6261
|
|
|
6226
6262
|
// src/discussion-reference-security.ts
|
|
6227
6263
|
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
@@ -7822,7 +7858,7 @@ var handleCrm = async (req, url, env, ctx) => {
|
|
|
7822
7858
|
const crmBase = ctx.crmBase;
|
|
7823
7859
|
if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + "/")) return null;
|
|
7824
7860
|
const db = ctx.makeDb(env);
|
|
7825
|
-
const routes = (0,
|
|
7861
|
+
const routes = (0, import_crm10.createCrmRoutes)({
|
|
7826
7862
|
crm: ctx.chapter.crm,
|
|
7827
7863
|
db,
|
|
7828
7864
|
authorize: async (r) => {
|
|
@@ -9233,7 +9269,7 @@ async function networkWriteAllowed(req, db, user, ctx, sensitiveRead = false) {
|
|
|
9233
9269
|
|
|
9234
9270
|
// src/network-person-reference.ts
|
|
9235
9271
|
var import_db6 = require("@odla-ai/db");
|
|
9236
|
-
var
|
|
9272
|
+
var import_crm12 = require("@odla-ai/crm");
|
|
9237
9273
|
|
|
9238
9274
|
// src/worker-network-fetch.ts
|
|
9239
9275
|
var NetworkTargetTransportError = class extends Error {
|
|
@@ -9269,7 +9305,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
9269
9305
|
}
|
|
9270
9306
|
|
|
9271
9307
|
// src/worker-network-admission-state.ts
|
|
9272
|
-
var
|
|
9308
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
9273
9309
|
var MAX_ASSIGNMENTS = 2e3;
|
|
9274
9310
|
var admissionRows = (value) => Array.isArray(value) ? value : [];
|
|
9275
9311
|
function assignmentId(targetId, personId) {
|
|
@@ -9296,7 +9332,7 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
9296
9332
|
return isValidEmail(normalized) ? normalized : null;
|
|
9297
9333
|
}
|
|
9298
9334
|
async function grantInput(db, ctx, assignment, resolveEmail) {
|
|
9299
|
-
const person = resolveEmail ? null : await (0,
|
|
9335
|
+
const person = resolveEmail ? null : await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
|
|
9300
9336
|
const email = resolveEmail ? await resolveEmail(assignment.personId) : person ? personAdmissionEmail(ctx, person) : null;
|
|
9301
9337
|
if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
|
|
9302
9338
|
return {
|
|
@@ -9502,7 +9538,7 @@ function chapterPersonReference(targetId, recordId) {
|
|
|
9502
9538
|
}
|
|
9503
9539
|
async function resolveAdmissionPersonEmail(db, env, ctx, target, personId) {
|
|
9504
9540
|
if (!personId.startsWith("chapter-person:")) {
|
|
9505
|
-
const person2 = await (0,
|
|
9541
|
+
const person2 = await (0, import_crm12.getRecord)({ crm: ctx.chapter.crm, db }, personId);
|
|
9506
9542
|
return person2 ? personAdmissionEmail(ctx, person2) : null;
|
|
9507
9543
|
}
|
|
9508
9544
|
const prefix = `chapter-person:${target.id}:`;
|
|
@@ -11401,10 +11437,10 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
11401
11437
|
};
|
|
11402
11438
|
|
|
11403
11439
|
// src/worker-routes-people-profile.ts
|
|
11404
|
-
var
|
|
11440
|
+
var import_crm14 = require("@odla-ai/crm");
|
|
11405
11441
|
|
|
11406
11442
|
// src/people-profile.ts
|
|
11407
|
-
var
|
|
11443
|
+
var import_crm13 = require("@odla-ai/crm");
|
|
11408
11444
|
var sameValue = (a, b) => (a == null || a === "") && (b == null || b === "") || JSON.stringify(a) === JSON.stringify(b);
|
|
11409
11445
|
function profileReadOnlyFields(record4, ctx) {
|
|
11410
11446
|
return [
|
|
@@ -11419,17 +11455,17 @@ async function updateChapterPerson(opts) {
|
|
|
11419
11455
|
const { db, env, ctx, record: record4 } = opts;
|
|
11420
11456
|
const input = Object.fromEntries(Object.entries(opts.input).filter(([key3, value]) => !sameValue(value, record4.fields[key3])));
|
|
11421
11457
|
const forbidden = profileReadOnlyFields(record4, ctx).filter((key3) => key3 in input);
|
|
11422
|
-
if (forbidden.length) throw new
|
|
11458
|
+
if (forbidden.length) throw new import_crm13.CrmInputError(
|
|
11423
11459
|
"Use the verified identity or application workflow for protected fields",
|
|
11424
11460
|
Object.fromEntries(forbidden.map((key3) => [key3, "This field is managed by its authoritative workflow"]))
|
|
11425
11461
|
);
|
|
11426
11462
|
const appId = record4.fields.applicationId;
|
|
11427
11463
|
const app = typeof appId === "string" && appId ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : null;
|
|
11428
11464
|
if (appId && (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || String(app.email).trim().toLowerCase() !== String(record4.primaryEmail).trim().toLowerCase())) {
|
|
11429
|
-
throw new
|
|
11465
|
+
throw new import_crm13.CrmInputError("The application identity must be repaired before editing this profile");
|
|
11430
11466
|
}
|
|
11431
11467
|
const prepared = ctx.chapter.crm.prepare(record4.type, input, record4);
|
|
11432
|
-
if (!prepared.ok) throw new
|
|
11468
|
+
if (!prepared.ok) throw new import_crm13.CrmInputError("invalid input", prepared.errors);
|
|
11433
11469
|
const appAttrs = {};
|
|
11434
11470
|
if (app) {
|
|
11435
11471
|
const mirrored = /* @__PURE__ */ new Set(["firstName", "lastName", "phone", "linkedin", ...ctx.chapter.application.crmFields]);
|
|
@@ -11458,7 +11494,7 @@ async function updateChapterPerson(opts) {
|
|
|
11458
11494
|
}] });
|
|
11459
11495
|
}
|
|
11460
11496
|
};
|
|
11461
|
-
return (0,
|
|
11497
|
+
return (0, import_crm13.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
|
|
11462
11498
|
id: record4.id,
|
|
11463
11499
|
input,
|
|
11464
11500
|
expectedUpdatedAt: opts.expectedRevision ?? record4.updatedAt,
|
|
@@ -11503,7 +11539,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
11503
11539
|
} catch {
|
|
11504
11540
|
return json({ error: "invalid record id" }, 422);
|
|
11505
11541
|
}
|
|
11506
|
-
const record4 = await (0,
|
|
11542
|
+
const record4 = await (0, import_crm14.getRecord)({ crm: ctx.chapter.crm, db }, id7);
|
|
11507
11543
|
if (!record4) return json({ error: "not found" }, 404);
|
|
11508
11544
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
11509
11545
|
if (match?.[2]) {
|
|
@@ -11555,7 +11591,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
11555
11591
|
});
|
|
11556
11592
|
return json(result);
|
|
11557
11593
|
} catch (error) {
|
|
11558
|
-
if (error instanceof
|
|
11594
|
+
if (error instanceof import_crm14.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
|
|
11559
11595
|
if (error.code === "transact_guard_failed") return json({ error: "profile changed; reload before saving" }, 409);
|
|
11560
11596
|
return json({ error: "profile update failed" }, 503);
|
|
11561
11597
|
}
|
|
@@ -11730,10 +11766,10 @@ async function repairAccountPeoplePage(deps, after) {
|
|
|
11730
11766
|
}
|
|
11731
11767
|
|
|
11732
11768
|
// src/crm-application-repair.ts
|
|
11733
|
-
var
|
|
11769
|
+
var import_crm15 = require("@odla-ai/crm");
|
|
11734
11770
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
11735
11771
|
const deps = { crm: chapter.crm, db };
|
|
11736
|
-
const record4 = await (0,
|
|
11772
|
+
const record4 = await (0, import_crm15.getRecord)(deps, recordId);
|
|
11737
11773
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
11738
11774
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
11739
11775
|
const fields = record4.fields;
|
|
@@ -11751,7 +11787,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
11751
11787
|
}
|
|
11752
11788
|
const app = [...applications.values()][0];
|
|
11753
11789
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
11754
|
-
await (0,
|
|
11790
|
+
await (0, import_crm15.updateRecord)(deps, {
|
|
11755
11791
|
id: recordId,
|
|
11756
11792
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
11757
11793
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -12572,10 +12608,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
12572
12608
|
};
|
|
12573
12609
|
|
|
12574
12610
|
// src/email-admin-config.ts
|
|
12575
|
-
var
|
|
12611
|
+
var import_crm17 = require("@odla-ai/crm");
|
|
12576
12612
|
|
|
12577
12613
|
// src/email-readiness.ts
|
|
12578
|
-
var
|
|
12614
|
+
var import_crm16 = require("@odla-ai/crm");
|
|
12579
12615
|
|
|
12580
12616
|
// src/email-spam-signals.ts
|
|
12581
12617
|
var TRIGGER_PHRASES = [
|
|
@@ -12612,6 +12648,7 @@ var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|
|
|
|
12612
12648
|
function emailSpamSignals(copy) {
|
|
12613
12649
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12614
12650
|
const text10 = typeof copy.text === "string" ? copy.text : "";
|
|
12651
|
+
const html = typeof copy.html === "string" ? copy.html : "";
|
|
12615
12652
|
const out = [];
|
|
12616
12653
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
12617
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, "")));
|
|
@@ -12624,7 +12661,8 @@ function emailSpamSignals(copy) {
|
|
|
12624
12661
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12625
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." });
|
|
12626
12663
|
if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
12627
|
-
const urls = [
|
|
12664
|
+
const urls = [...`${text10}
|
|
12665
|
+
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
12628
12666
|
const hosts = urls.map((url) => {
|
|
12629
12667
|
try {
|
|
12630
12668
|
return new URL(url).hostname;
|
|
@@ -12661,18 +12699,22 @@ function validateEmailReadiness(input) {
|
|
|
12661
12699
|
}
|
|
12662
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." });
|
|
12663
12701
|
for (const tier of input.tiers ?? []) {
|
|
12664
|
-
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.` });
|
|
12665
12703
|
}
|
|
12666
|
-
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));
|
|
12667
12705
|
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
12668
12706
|
const events = Object.entries(input.events).map(([key3, event2]) => {
|
|
12669
12707
|
const template = input.templates[key3];
|
|
12670
|
-
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 });
|
|
12671
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 }));
|
|
12672
12712
|
if (template) {
|
|
12673
|
-
const
|
|
12674
|
-
${template.text ?? ""}
|
|
12675
|
-
|
|
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)) {
|
|
12676
12718
|
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
12677
12719
|
}
|
|
12678
12720
|
}
|
|
@@ -12694,13 +12736,14 @@ var str2 = (value) => typeof value === "string" ? value : "";
|
|
|
12694
12736
|
async function emailSettings(db, ctx, env) {
|
|
12695
12737
|
const hub = ctx.chapter.mode === "hub";
|
|
12696
12738
|
const crmDeps3 = { db, crm: ctx.chapter.crm };
|
|
12697
|
-
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];
|
|
12698
12740
|
const raw2 = group?.emailTemplates;
|
|
12699
12741
|
const stored = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
|
|
12700
12742
|
const events = chapterEmailEvents(ctx.chapter);
|
|
12701
12743
|
const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
|
|
12702
12744
|
subject: str2(stored[key3]?.subject),
|
|
12703
12745
|
text: str2(stored[key3]?.text),
|
|
12746
|
+
html: str2(stored[key3]?.html),
|
|
12704
12747
|
enabled: stored[key3]?.enabled
|
|
12705
12748
|
}]));
|
|
12706
12749
|
let tiers = [], tiersUnavailable = false;
|
|
@@ -12747,12 +12790,14 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12747
12790
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12748
12791
|
const t = value;
|
|
12749
12792
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12750
|
-
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
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);
|
|
12754
12799
|
}
|
|
12755
|
-
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12800
|
+
clean[key3] = { subject, text: text10, ...html.trim() ? { html } : {}, enabled: t.enabled };
|
|
12756
12801
|
}
|
|
12757
12802
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12758
12803
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -12765,7 +12810,7 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12765
12810
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
12766
12811
|
if (ctx.chapter.mode === "hub") {
|
|
12767
12812
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
12768
|
-
await (0,
|
|
12813
|
+
await (0, import_crm17.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
12769
12814
|
} else {
|
|
12770
12815
|
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
12771
12816
|
await db.transact([
|
|
@@ -12778,7 +12823,7 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12778
12823
|
}
|
|
12779
12824
|
|
|
12780
12825
|
// src/worker-routes-admin-comms.ts
|
|
12781
|
-
var
|
|
12826
|
+
var import_crm18 = require("@odla-ai/crm");
|
|
12782
12827
|
async function gate5(req, env, ctx) {
|
|
12783
12828
|
const rawDb = ctx.makeDb(env);
|
|
12784
12829
|
const u = await ctx.verifyUser(req, env);
|
|
@@ -12811,7 +12856,7 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
|
12811
12856
|
const got = await gate5(req, env, ctx);
|
|
12812
12857
|
if (got instanceof Response) return got;
|
|
12813
12858
|
if (ctx.chapter.mode === "hub") {
|
|
12814
|
-
const rows5 = await (0,
|
|
12859
|
+
const rows5 = await (0, import_crm18.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
|
|
12815
12860
|
return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
|
|
12816
12861
|
}
|
|
12817
12862
|
const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
|
|
@@ -12908,7 +12953,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
12908
12953
|
};
|
|
12909
12954
|
|
|
12910
12955
|
// src/worker-routes-network.ts
|
|
12911
|
-
var
|
|
12956
|
+
var import_crm20 = require("@odla-ai/crm");
|
|
12912
12957
|
var import_db9 = require("@odla-ai/db");
|
|
12913
12958
|
|
|
12914
12959
|
// src/network-edge.ts
|
|
@@ -12934,7 +12979,7 @@ function unpairedResponse(unpaired) {
|
|
|
12934
12979
|
}
|
|
12935
12980
|
|
|
12936
12981
|
// src/worker-network-push.ts
|
|
12937
|
-
var
|
|
12982
|
+
var import_crm19 = require("@odla-ai/crm");
|
|
12938
12983
|
var import_db8 = require("@odla-ai/db");
|
|
12939
12984
|
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
12940
12985
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12946,7 +12991,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12946
12991
|
};
|
|
12947
12992
|
if (!secret) {
|
|
12948
12993
|
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
12949
|
-
await (0,
|
|
12994
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12950
12995
|
recordId: record4.id,
|
|
12951
12996
|
targetId: target.id,
|
|
12952
12997
|
status: "failed",
|
|
@@ -12985,7 +13030,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12985
13030
|
const responseBody = await res.json().catch(() => ({}));
|
|
12986
13031
|
if (!res.ok) {
|
|
12987
13032
|
const error = responseBody.error ?? "follower rejected the record";
|
|
12988
|
-
await (0,
|
|
13033
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12989
13034
|
recordId: record4.id,
|
|
12990
13035
|
targetId: target.id,
|
|
12991
13036
|
status: "failed",
|
|
@@ -13000,7 +13045,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
13000
13045
|
error
|
|
13001
13046
|
};
|
|
13002
13047
|
}
|
|
13003
|
-
await (0,
|
|
13048
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
13004
13049
|
recordId: record4.id,
|
|
13005
13050
|
targetId: target.id,
|
|
13006
13051
|
status: "delivered",
|
|
@@ -13016,7 +13061,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
13016
13061
|
};
|
|
13017
13062
|
} catch (err) {
|
|
13018
13063
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
13019
|
-
await (0,
|
|
13064
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
13020
13065
|
recordId: record4.id,
|
|
13021
13066
|
targetId: target.id,
|
|
13022
13067
|
status: "failed",
|
|
@@ -13069,7 +13114,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
13069
13114
|
const verified = await (0, import_db9.verifyFederatedRequest)(req, { secret });
|
|
13070
13115
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
13071
13116
|
const types = await Promise.all(
|
|
13072
|
-
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))
|
|
13073
13118
|
);
|
|
13074
13119
|
const activeConfiguration = ctx.chapter.signupControl ? await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV) : null;
|
|
13075
13120
|
const snapshot = {
|
|
@@ -13216,7 +13261,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
13216
13261
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
13217
13262
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
13218
13263
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
13219
|
-
const record4 = await (0,
|
|
13264
|
+
const record4 = await (0, import_crm20.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
13220
13265
|
if (!record4) return json({ error: "record not found" }, 404);
|
|
13221
13266
|
const results = await Promise.all(
|
|
13222
13267
|
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
@@ -13225,11 +13270,11 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
13225
13270
|
};
|
|
13226
13271
|
|
|
13227
13272
|
// src/worker-routes-network-records-follower.ts
|
|
13228
|
-
var
|
|
13273
|
+
var import_crm24 = require("@odla-ai/crm");
|
|
13229
13274
|
var import_db10 = require("@odla-ai/db");
|
|
13230
13275
|
|
|
13231
13276
|
// src/network-application-action.ts
|
|
13232
|
-
var
|
|
13277
|
+
var import_crm21 = require("@odla-ai/crm");
|
|
13233
13278
|
var ACTION_META = "chapterNetworkMutation";
|
|
13234
13279
|
async function networkApplicationAction(opts) {
|
|
13235
13280
|
const { db, env, ctx, record: record4, input, sourceId, activityId, fingerprint } = opts;
|
|
@@ -13319,7 +13364,7 @@ async function networkApplicationAction(opts) {
|
|
|
13319
13364
|
const current = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
|
|
13320
13365
|
if (!current || current.status !== input.stage) return json({ error: "application reconciliation pending", code: "action_pending" }, 503);
|
|
13321
13366
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) });
|
|
13322
|
-
const projected = await (0,
|
|
13367
|
+
const projected = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
13323
13368
|
if (!projected || projected.stage !== input.stage) return json({ error: "CRM reconciliation pending", code: "action_pending" }, 503);
|
|
13324
13369
|
const audit = (await db.query({ crm_activity: { $: { where: { id: activityId }, limit: 1 } } })).crm_activity?.[0];
|
|
13325
13370
|
const priorMeta = audit?.meta;
|
|
@@ -13329,7 +13374,7 @@ async function networkApplicationAction(opts) {
|
|
|
13329
13374
|
} },
|
|
13330
13375
|
{ t: "update", ns: "crm_activity", id: lockId, attrs: { meta: { state: "complete", activityId } } }
|
|
13331
13376
|
], { guards: [{ ns: "crm_activity", id: lockId, exists: true, equals: { meta: { state: "pending", activityId } } }] });
|
|
13332
|
-
const updated = await (0,
|
|
13377
|
+
const updated = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
13333
13378
|
if (!updated) return json({ error: "record not found" }, 404);
|
|
13334
13379
|
return { record: updated, duplicate: Boolean(receipt) };
|
|
13335
13380
|
}
|
|
@@ -13365,7 +13410,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
13365
13410
|
}
|
|
13366
13411
|
|
|
13367
13412
|
// src/worker-routes-network-records-search.ts
|
|
13368
|
-
var
|
|
13413
|
+
var import_crm22 = require("@odla-ai/crm");
|
|
13369
13414
|
var SEARCH_BATCH_SIZE = 1e3;
|
|
13370
13415
|
var MAX_READER_SEARCH_POPULATION = 1e4;
|
|
13371
13416
|
var ReaderSearchPopulationError = class extends Error {
|
|
@@ -13406,19 +13451,19 @@ async function listVisibleSearchPopulation(db, where, window, count) {
|
|
|
13406
13451
|
(_, index2) => index2 * SEARCH_BATCH_SIZE
|
|
13407
13452
|
);
|
|
13408
13453
|
const pages = await Promise.all(offsets.map((offset) => db.query({
|
|
13409
|
-
[
|
|
13454
|
+
[import_crm22.CRM_NS.record]: { $: {
|
|
13410
13455
|
where,
|
|
13411
13456
|
order: { [window.sort.field]: window.sort.dir },
|
|
13412
13457
|
limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
|
|
13413
13458
|
offset
|
|
13414
13459
|
} }
|
|
13415
13460
|
})));
|
|
13416
|
-
return pages.flatMap((page) => (page[
|
|
13461
|
+
return pages.flatMap((page) => (page[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord));
|
|
13417
13462
|
}
|
|
13418
13463
|
async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
13419
13464
|
const where = baseWhere(type, window.stage);
|
|
13420
13465
|
const crmDb = db;
|
|
13421
|
-
const aggregate = await crmDb.aggregate(
|
|
13466
|
+
const aggregate = await crmDb.aggregate(import_crm22.CRM_NS.record, { count: true }, { where });
|
|
13422
13467
|
const candidateCount = aggregate.count ?? 0;
|
|
13423
13468
|
if (window.search) {
|
|
13424
13469
|
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
@@ -13430,7 +13475,7 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
13430
13475
|
};
|
|
13431
13476
|
}
|
|
13432
13477
|
const result = await crmDb.query({
|
|
13433
|
-
[
|
|
13478
|
+
[import_crm22.CRM_NS.record]: { $: {
|
|
13434
13479
|
where,
|
|
13435
13480
|
order: { [window.sort.field]: window.sort.dir },
|
|
13436
13481
|
limit: window.limit,
|
|
@@ -13438,19 +13483,19 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
13438
13483
|
} }
|
|
13439
13484
|
});
|
|
13440
13485
|
return {
|
|
13441
|
-
records: (result[
|
|
13486
|
+
records: (result[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord),
|
|
13442
13487
|
total: candidateCount
|
|
13443
13488
|
};
|
|
13444
13489
|
}
|
|
13445
13490
|
|
|
13446
13491
|
// src/network-mutation-receipt.ts
|
|
13447
|
-
var
|
|
13492
|
+
var import_crm23 = require("@odla-ai/crm");
|
|
13448
13493
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
13449
13494
|
async function priorMutation(db, id7) {
|
|
13450
13495
|
const result = await db.query({
|
|
13451
|
-
[
|
|
13496
|
+
[import_crm23.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
13452
13497
|
});
|
|
13453
|
-
const row2 = (result[
|
|
13498
|
+
const row2 = (result[import_crm23.CRM_NS.activity] ?? [])[0];
|
|
13454
13499
|
const meta = row2?.meta;
|
|
13455
13500
|
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
13456
13501
|
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint, pending: receipt.state === "pending", failed: receipt.state === "failed" } : null;
|
|
@@ -13474,7 +13519,7 @@ function mutationError(error) {
|
|
|
13474
13519
|
if (error?.code === "transact_guard_failed") {
|
|
13475
13520
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
13476
13521
|
}
|
|
13477
|
-
if (error instanceof
|
|
13522
|
+
if (error instanceof import_crm24.CrmInputError) {
|
|
13478
13523
|
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
13479
13524
|
}
|
|
13480
13525
|
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
@@ -13529,7 +13574,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13529
13574
|
}
|
|
13530
13575
|
if (prior?.failed) return json({ error: "this operation was not applied; reload before choosing a new action", code: "action_failed" }, 409);
|
|
13531
13576
|
if (prior && !prior.pending) {
|
|
13532
|
-
const replayed = await (0,
|
|
13577
|
+
const replayed = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
13533
13578
|
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
13534
13579
|
const result = {
|
|
13535
13580
|
version: 1,
|
|
@@ -13539,7 +13584,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13539
13584
|
};
|
|
13540
13585
|
return json(result);
|
|
13541
13586
|
}
|
|
13542
|
-
const record4 = await (0,
|
|
13587
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
13543
13588
|
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
13544
13589
|
if (!prior?.pending && record4.updatedAt !== input.expectedRevision) {
|
|
13545
13590
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
@@ -13602,7 +13647,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13602
13647
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
13603
13648
|
});
|
|
13604
13649
|
}
|
|
13605
|
-
const updated = hasFields ? await (0,
|
|
13650
|
+
const updated = hasFields ? await (0, import_crm24.updateRecord)(deps, {
|
|
13606
13651
|
id: record4.id,
|
|
13607
13652
|
input: fields,
|
|
13608
13653
|
mutationId: activityId,
|
|
@@ -13613,7 +13658,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13613
13658
|
authorId: `network:${verified.sender}`,
|
|
13614
13659
|
meta: auditMeta
|
|
13615
13660
|
}
|
|
13616
|
-
}) : await (0,
|
|
13661
|
+
}) : await (0, import_crm24.setStage)(deps, {
|
|
13617
13662
|
id: record4.id,
|
|
13618
13663
|
to: input.stage,
|
|
13619
13664
|
authorId: `network:${verified.sender}`,
|
|
@@ -13640,7 +13685,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13640
13685
|
const recordId = url.searchParams.get("recordId");
|
|
13641
13686
|
if (recordId !== null) {
|
|
13642
13687
|
if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
|
|
13643
|
-
const record4 = await (0,
|
|
13688
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
13644
13689
|
if (!record4 || record4.type !== type || record4.archived) return json({ error: "record not found" }, 404);
|
|
13645
13690
|
return json({
|
|
13646
13691
|
version: 1,
|
|
@@ -13935,7 +13980,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
13935
13980
|
};
|
|
13936
13981
|
|
|
13937
13982
|
// src/worker-routes-network-shared-notes.ts
|
|
13938
|
-
var
|
|
13983
|
+
var import_crm25 = require("@odla-ai/crm");
|
|
13939
13984
|
var import_db12 = require("@odla-ai/db");
|
|
13940
13985
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
13941
13986
|
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
@@ -13966,7 +14011,7 @@ function sharedNote(activity, sourceId) {
|
|
|
13966
14011
|
}
|
|
13967
14012
|
async function listSharedNotes(db, sourceId, recordId) {
|
|
13968
14013
|
const result = await db.query({
|
|
13969
|
-
[
|
|
14014
|
+
[import_crm25.CRM_NS.activity]: {
|
|
13970
14015
|
$: {
|
|
13971
14016
|
where: {
|
|
13972
14017
|
and: [
|
|
@@ -13979,7 +14024,7 @@ async function listSharedNotes(db, sourceId, recordId) {
|
|
|
13979
14024
|
}
|
|
13980
14025
|
}
|
|
13981
14026
|
});
|
|
13982
|
-
return (result[
|
|
14027
|
+
return (result[import_crm25.CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
13983
14028
|
const note = sharedNote(activity, sourceId);
|
|
13984
14029
|
return note ? [note] : [];
|
|
13985
14030
|
});
|
|
@@ -14007,7 +14052,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
14007
14052
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
14008
14053
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
14009
14054
|
}
|
|
14010
|
-
const record4 = await (0,
|
|
14055
|
+
const record4 = await (0, import_crm25.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
14011
14056
|
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
14012
14057
|
if (req.method === "GET") {
|
|
14013
14058
|
return json({
|
|
@@ -14029,7 +14074,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
14029
14074
|
}
|
|
14030
14075
|
const createdAt = Date.now();
|
|
14031
14076
|
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
14032
|
-
const result = await (0,
|
|
14077
|
+
const result = await (0, import_crm25.addActivity)(
|
|
14033
14078
|
{
|
|
14034
14079
|
crm: ctx.chapter.crm,
|
|
14035
14080
|
db,
|
|
@@ -15658,7 +15703,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15658
15703
|
};
|
|
15659
15704
|
|
|
15660
15705
|
// src/worker-routes-formation.ts
|
|
15661
|
-
var
|
|
15706
|
+
var import_crm26 = require("@odla-ai/crm");
|
|
15662
15707
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
15663
15708
|
var handleFormation = async (req, url, env, ctx) => {
|
|
15664
15709
|
const formation = ctx.chapter.formation;
|
|
@@ -15710,7 +15755,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
15710
15755
|
const db = ctx.makeDb(env);
|
|
15711
15756
|
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
15712
15757
|
try {
|
|
15713
|
-
const created = await (0,
|
|
15758
|
+
const created = await (0, import_crm26.createRecord)(
|
|
15714
15759
|
{
|
|
15715
15760
|
crm: ctx.chapter.crm,
|
|
15716
15761
|
db,
|
|
@@ -17901,7 +17946,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17901
17946
|
}
|
|
17902
17947
|
|
|
17903
17948
|
// src/membership-chapter-delivery.ts
|
|
17904
|
-
var
|
|
17949
|
+
var import_crm27 = require("@odla-ai/crm");
|
|
17905
17950
|
|
|
17906
17951
|
// src/invitation-test-delivery.ts
|
|
17907
17952
|
function invitationDelivery(env, recipientEmail) {
|
|
@@ -17966,7 +18011,7 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
17966
18011
|
join.searchParams.set("seat", effect.seatId);
|
|
17967
18012
|
join.searchParams.set("tier", effect.skuId);
|
|
17968
18013
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
17969
|
-
if (template && !(0,
|
|
18014
|
+
if (template && !(0, import_crm27.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
|
|
17970
18015
|
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: {
|
|
17971
18016
|
...template,
|
|
17972
18017
|
text: "{{purchaserName}} has purchased a membership for you at {{chapterName}}.\n\n" + template.text
|