@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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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, import_crm5.validateEmailCopy)(tpl, {
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
- if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
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, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
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
- return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
1787
- }
1788
-
1789
- // src/email-html.ts
1790
- var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
1791
- function escapeHtml(s) {
1792
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 import_crm7 = require("@odla-ai/crm");
1861
+ var import_crm8 = require("@odla-ai/crm");
1832
1862
 
1833
1863
  // src/email-event-values.ts
1834
- var import_crm6 = require("@odla-ai/crm");
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, import_crm6.isEmailTierName)(name, tierId) ? name.trim() : "";
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, import_crm7.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
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 import_crm8 = require("@odla-ai/crm");
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, import_crm8.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
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, import_crm8.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
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 import_crm9 = require("@odla-ai/crm");
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, import_crm9.createCrmRoutes)({
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 import_crm11 = require("@odla-ai/crm");
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 import_crm10 = require("@odla-ai/crm");
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, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
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, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, personId);
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 import_crm13 = require("@odla-ai/crm");
11440
+ var import_crm14 = require("@odla-ai/crm");
11405
11441
 
11406
11442
  // src/people-profile.ts
11407
- var import_crm12 = require("@odla-ai/crm");
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 import_crm12.CrmInputError(
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 import_crm12.CrmInputError("The application identity must be repaired before editing this profile");
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 import_crm12.CrmInputError("invalid input", prepared.errors);
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, import_crm12.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
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, import_crm13.getRecord)({ crm: ctx.chapter.crm, db }, id7);
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 import_crm13.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
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 import_crm14 = require("@odla-ai/crm");
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, import_crm14.getRecord)(deps, recordId);
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, import_crm14.updateRecord)(deps, {
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 import_crm16 = require("@odla-ai/crm");
12611
+ var import_crm17 = require("@odla-ai/crm");
12576
12612
 
12577
12613
  // src/email-readiness.ts
12578
- var import_crm15 = require("@odla-ai/crm");
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 = [...text10.matchAll(URL_RE2)].map((match) => match[0]);
12664
+ const urls = [...`${text10}
12665
+ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
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, import_crm15.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
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, import_crm15.isEmailTierName)(tier.name, tier.id));
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, import_crm15.validateEmailCopy)(template, { allowedVariables: event2.variables });
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 used = [...`${template.subject ?? ""}
12674
- ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
12675
- for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
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, import_crm16.getConfig)(crmDeps3) : (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[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
- const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text10 }, { allowedVariables: events[key3].variables });
12752
- if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
12753
- return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
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, import_crm16.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
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 import_crm17 = require("@odla-ai/crm");
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, import_crm17.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
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 import_crm19 = require("@odla-ai/crm");
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 import_crm18 = require("@odla-ai/crm");
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, import_crm18.recordDeliveryAttempt)(crmDeps3, {
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, import_crm18.recordDeliveryAttempt)(crmDeps3, {
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, import_crm18.recordDeliveryAttempt)(crmDeps3, {
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, import_crm18.recordDeliveryAttempt)(crmDeps3, {
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, import_crm19.typeSummary)({ crm: ctx.chapter.crm, db }, type))
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, import_crm19.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
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 import_crm23 = require("@odla-ai/crm");
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 import_crm20 = require("@odla-ai/crm");
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, import_crm20.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
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, import_crm20.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
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 import_crm21 = require("@odla-ai/crm");
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
- [import_crm21.CRM_NS.record]: { $: {
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[import_crm21.CRM_NS.record] ?? []).map(import_crm21.rowToRecord));
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(import_crm21.CRM_NS.record, { count: true }, { where });
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
- [import_crm21.CRM_NS.record]: { $: {
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[import_crm21.CRM_NS.record] ?? []).map(import_crm21.rowToRecord),
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 import_crm22 = require("@odla-ai/crm");
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
- [import_crm22.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
13496
+ [import_crm23.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
13452
13497
  });
13453
- const row2 = (result[import_crm22.CRM_NS.activity] ?? [])[0];
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 import_crm23.CrmInputError) {
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, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
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, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
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, import_crm23.updateRecord)(deps, {
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, import_crm23.setStage)(deps, {
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, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
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 import_crm24 = require("@odla-ai/crm");
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
- [import_crm24.CRM_NS.activity]: {
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[import_crm24.CRM_NS.activity] ?? []).flatMap((activity) => {
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, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
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, import_crm24.addActivity)(
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 import_crm25 = require("@odla-ai/crm");
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, import_crm25.createRecord)(
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 import_crm26 = require("@odla-ai/crm");
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, import_crm26.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
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