@odla-ai/chapter 0.60.0 → 0.62.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.
@@ -4,8 +4,8 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
6
  var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
9
  };
10
10
  var __copyProps = (to, from, except, desc) => {
11
11
  if (from && typeof from === "object" || typeof from === "function") {
@@ -70,6 +70,20 @@ __export(worker_exports, {
70
70
  });
71
71
  module.exports = __toCommonJS(worker_exports);
72
72
 
73
+ // src/admin-second-factor.ts
74
+ function sessionHasSecondFactor(payload) {
75
+ const fva = payload?.fva;
76
+ return Array.isArray(fva) && typeof fva[1] === "number" && fva[1] >= 0;
77
+ }
78
+ function adminSecondFactorAllows(mode, payload, who) {
79
+ if (mode === "off" || sessionHasSecondFactor(payload)) return true;
80
+ if (mode === "report") {
81
+ console.warn(JSON.stringify({ event: "chapter.admin_second_factor_missing", mode, who: who ?? null }));
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+
73
87
  // src/worker-context.ts
74
88
  var import_db = require("@odla-ai/db");
75
89
  var import_jose = require("jose");
@@ -99,9 +113,9 @@ function canChangeRole(ctx) {
99
113
  }
100
114
  return { ok: true };
101
115
  }
102
- async function getVaultSecret(db, name) {
116
+ async function getVaultSecret(db, name2) {
103
117
  try {
104
- const value = await db.secrets.get(name);
118
+ const value = await db.secrets.get(name2);
105
119
  return typeof value === "string" && value !== "" ? value : void 0;
106
120
  } catch {
107
121
  return void 0;
@@ -434,7 +448,8 @@ function createWorkerContext(options) {
434
448
  return await isAdminEmail(db, u.email) ? auth.adminRole : auth.ladder[0];
435
449
  }
436
450
  async function isAdmin(db, u) {
437
- return await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
451
+ const admin = await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
452
+ return admin && adminSecondFactorAllows(auth.adminSecondFactor ?? "off", u.payload, u.email ?? u.userId);
438
453
  }
439
454
  function crmSender(env) {
440
455
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return void 0;
@@ -922,14 +937,14 @@ function projectNetworkCommercialEvents(input, sources) {
922
937
  const prior = latest.get(key3);
923
938
  if (!prior || order(prior, fact) <= 0) latest.set(key3, row2);
924
939
  }
925
- const rows5 = [...latest.values()];
926
- const customers = rows5.filter((row2) => row2.objectKind === "customer");
927
- const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
928
- const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
929
- const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
930
- const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
931
- const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
932
- for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
940
+ const rows6 = [...latest.values()];
941
+ const customers = rows6.filter((row2) => row2.objectKind === "customer");
942
+ const subscriptions = rows6.filter((row2) => row2.objectKind === "subscription");
943
+ const invoices = rows6.filter((row2) => row2.objectKind === "invoice");
944
+ const seatPayments = rows6.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
945
+ const disputes = rows6.filter((row2) => row2.objectKind === "dispute");
946
+ const refunds = rows6.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
947
+ for (const charge of rows6.filter((row2) => row2.eventType === "charge.refunded")) {
933
948
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
934
949
  exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
935
950
  }
@@ -1187,11 +1202,11 @@ async function readStripeRecoveryEvent(db, eventId) {
1187
1202
  }
1188
1203
  async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
1189
1204
  const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
1190
- const rows5 = await db.query({
1205
+ const rows6 = await db.query({
1191
1206
  networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
1192
1207
  networkCommercialExceptions: { $: { where: { id: exceptionId2 }, limit: 1 } }
1193
1208
  });
1194
- const stored = rows5.networkCommercialEvents?.[0], exception = rows5.networkCommercialExceptions?.[0];
1209
+ const stored = rows6.networkCommercialEvents?.[0], exception = rows6.networkCommercialExceptions?.[0];
1195
1210
  if (!stored) throw new Error("stored event not found");
1196
1211
  const provider = await readStripeRecoveryEvent(db, eventId);
1197
1212
  const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
@@ -1267,7 +1282,7 @@ function findGiftRef(obj) {
1267
1282
  const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
1268
1283
  const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
1269
1284
  const candidates = [metaOf(obj), metaOf(subscription)];
1270
- const pickIdentity = (name) => candidates.map((metadata) => typeof metadata[name] === "string" ? String(metadata[name]) : void 0).find(Boolean);
1285
+ const pickIdentity = (name2) => candidates.map((metadata) => typeof metadata[name2] === "string" ? String(metadata[name2]) : void 0).find(Boolean);
1271
1286
  const appId = pickIdentity("odlaAppId");
1272
1287
  const environment = pickIdentity("odlaEnvironment");
1273
1288
  const dataEnvironment = pickIdentity("odlaDataEnvironment");
@@ -1364,11 +1379,11 @@ async function archivedChapterMembershipEvent(db, rawBody, app, env, ctx) {
1364
1379
  if (!app.membershipRestartId) return false;
1365
1380
  const fact = normalizeNetworkCommercialStripeEvent(JSON.parse(rawBody), "");
1366
1381
  if (!fact.subscriptionId || fact.subscriptionId === app.stripeSubscriptionId) return false;
1367
- const rows5 = (await db.query({ membershipCheckoutHistory: { $: { where: {
1382
+ const rows6 = (await db.query({ membershipCheckoutHistory: { $: { where: {
1368
1383
  applicationId: app.id,
1369
1384
  providerSubscriptionId: fact.subscriptionId
1370
1385
  }, limit: 2 } } })).membershipCheckoutHistory ?? [];
1371
- return rows5.length === 1 && rows5[0].appId === env.ODLA_APP_ID && rows5[0].chapterId === ctx.chapter.id && rows5[0].environment === env.ODLA_ENV && rows5[0].runtime === stripeRuntimeIdentity(env.ODLA_RUNTIME);
1386
+ return rows6.length === 1 && rows6[0].appId === env.ODLA_APP_ID && rows6[0].chapterId === ctx.chapter.id && rows6[0].environment === env.ODLA_ENV && rows6[0].runtime === stripeRuntimeIdentity(env.ODLA_RUNTIME);
1372
1387
  }
1373
1388
 
1374
1389
  // src/crm-sync.ts
@@ -1559,10 +1574,10 @@ async function findSharedRecord(deps, record4, tag) {
1559
1574
  if (found.crm_record?.[0]) return found.crm_record[0];
1560
1575
  }
1561
1576
  const nameField = def.nameField ?? "name";
1562
- const name = record4.input[nameField];
1563
- if (record4.type === "company" && typeof name === "string" && name.trim()) {
1577
+ const name2 = record4.input[nameField];
1578
+ if (record4.type === "company" && typeof name2 === "string" && name2.trim()) {
1564
1579
  const found = await deps.db.query({
1565
- crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
1580
+ crm_record: { $: { where: { type: record4.type, name: name2.trim() }, limit: 1 } }
1566
1581
  });
1567
1582
  if (found.crm_record?.[0]) return found.crm_record[0];
1568
1583
  }
@@ -1700,27 +1715,81 @@ async function backfillCrm(deps) {
1700
1715
  }
1701
1716
 
1702
1717
  // src/email-retirement.ts
1703
- function isRetiredEmailCopy(subject, text10) {
1718
+ function isRetiredEmailCopy(subject, text11) {
1704
1719
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1705
- const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1720
+ const bodyCopy = text11.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1706
1721
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1707
1722
  }
1708
1723
 
1709
1724
  // src/email.ts
1725
+ var import_crm6 = require("@odla-ai/crm");
1726
+
1727
+ // src/email-render.ts
1710
1728
  var import_crm5 = require("@odla-ai/crm");
1711
1729
 
1730
+ // src/email-html.ts
1731
+ var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
1732
+ function escapeHtml(s) {
1733
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1734
+ }
1735
+ function lineHtml(line) {
1736
+ let out = "", last = 0;
1737
+ for (const match of line.matchAll(URL_RE)) {
1738
+ out += escapeHtml(line.slice(last, match.index));
1739
+ const url = escapeHtml(match[0]);
1740
+ out += `<a href="${url}">${url}</a>`;
1741
+ last = match.index + match[0].length;
1742
+ }
1743
+ return out + escapeHtml(line.slice(last));
1744
+ }
1745
+ function textToHtml(text11) {
1746
+ const paragraphs = text11.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1747
+ const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
1748
+ return `<!doctype html>
1749
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
1750
+ <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">
1751
+ ${body}
1752
+ </body></html>`;
1753
+ }
1754
+
1755
+ // src/email-render.ts
1756
+ function render(template, vars) {
1757
+ return (0, import_crm5.renderTemplate)(template, vars);
1758
+ }
1759
+ var HTML_BODY_LIMIT = 2e5;
1760
+ function renderHtmlBody(template, vars) {
1761
+ const escaped = Object.fromEntries(Object.entries(vars).map(([name2, value]) => [name2, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
1762
+ return (0, import_crm5.renderTemplate)(template, escaped);
1763
+ }
1764
+ function htmlBodyIssues(html, options) {
1765
+ const issues = [];
1766
+ if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
1767
+ for (const message of (0, import_crm5.templateSectionIssues)(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
1768
+ const unknown = new Set((0, import_crm5.templateVars)(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
1769
+ for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
1770
+ if (options.values) {
1771
+ for (const name2 of (0, import_crm5.templateSectionNames)(html)) if (typeof options.values[name2] !== "string") issues.push({ code: "value-missing", variable: name2, message: `Missing event value: ${name2} (HTML body).` });
1772
+ for (const variable of (0, import_crm5.templateLiveVars)(html, options.values)) {
1773
+ if (unknown.has(variable)) continue;
1774
+ const value = options.values[variable];
1775
+ if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
1776
+ }
1777
+ }
1778
+ return issues;
1779
+ }
1780
+
1712
1781
  // src/email-events.ts
1713
1782
  var identity = ["firstName", "lastName", "email", "phone", "state"];
1714
1783
  var policy = ["refundPolicyText", "commitmentText", "normsText"];
1715
- var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
1716
- var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
1784
+ var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
1785
+ var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
1717
1786
  var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
1718
1787
  var CHAPTER_EMAIL_EVENTS = {
1719
1788
  adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
1720
1789
  submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
1721
1790
  paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
1722
1791
  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"]),
1792
+ onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
1724
1793
  namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
1725
1794
  seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
1726
1795
  };
@@ -1735,9 +1804,6 @@ function chapterEmailEvents(chapter) {
1735
1804
  }
1736
1805
 
1737
1806
  // src/email.ts
1738
- function render(template, vars) {
1739
- return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
1740
- }
1741
1807
  function groupVars(group, vars) {
1742
1808
  return {
1743
1809
  ...vars,
@@ -1763,14 +1829,16 @@ function planDelivery(input) {
1763
1829
  const vars = groupVars(input.group, input.vars);
1764
1830
  if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
1765
1831
  const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
1766
- const issues = (0, import_crm5.validateEmailCopy)(tpl, {
1832
+ const issues = (0, import_crm6.validateEmailCopy)(tpl, {
1767
1833
  allowedVariables: event2?.variables,
1768
1834
  optionalVariables: event2?.optionalVariables,
1769
1835
  requiredVariables: event2?.requiresTier ? ["tierName"] : [],
1770
1836
  values: vars,
1771
1837
  tierId: vars.tierId
1772
1838
  });
1773
- if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
1839
+ const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
1840
+ const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
1841
+ if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
1774
1842
  const isProd = input.envName === "prod";
1775
1843
  const redirect = !isProd && !!input.group.debugEmail;
1776
1844
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
@@ -1778,37 +1846,14 @@ function planDelivery(input) {
1778
1846
  const renderedSubject = render(tpl.subject, vars);
1779
1847
  const renderedText = render(tpl.text, vars);
1780
1848
  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" };
1849
+ if ((0, import_crm6.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1782
1850
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1783
- const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
1851
+ const text11 = redirect ? `(dev redirect; original recipient: ${input.to})
1784
1852
 
1785
1853
  ` + 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>`;
1854
+ const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
1855
+ 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;
1856
+ return { deliver: true, transport, to, subject, text: text11, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
1812
1857
  }
1813
1858
 
1814
1859
  // src/email-deliverability.ts
@@ -1828,10 +1873,10 @@ function deliverabilityHeaders(input) {
1828
1873
  }
1829
1874
 
1830
1875
  // src/notify.ts
1831
- var import_crm7 = require("@odla-ai/crm");
1876
+ var import_crm8 = require("@odla-ai/crm");
1832
1877
 
1833
1878
  // src/email-event-values.ts
1834
- var import_crm6 = require("@odla-ai/crm");
1879
+ var import_crm7 = require("@odla-ai/crm");
1835
1880
 
1836
1881
  // src/tiers.ts
1837
1882
  function tierIsFree(tier) {
@@ -1844,9 +1889,9 @@ function tierPayable(tier, group, hasSecretKey) {
1844
1889
  function byOrder(a, b) {
1845
1890
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1846
1891
  }
1847
- function resolveTiers(group, rows5 = []) {
1848
- const stored = rows5.filter((tier) => tier.active);
1849
- if (rows5.length) return [...stored].sort(byOrder);
1892
+ function resolveTiers(group, rows6 = []) {
1893
+ const stored = rows6.filter((tier) => tier.active);
1894
+ if (rows6.length) return [...stored].sort(byOrder);
1850
1895
  return [{
1851
1896
  id: "standard",
1852
1897
  groupId: String(group.id ?? ""),
@@ -1902,16 +1947,16 @@ function rowToTier(row2, groupId) {
1902
1947
  }
1903
1948
  async function loadTiers(db, group) {
1904
1949
  const groupId = String(group.id ?? "");
1905
- let rows5;
1950
+ let rows6;
1906
1951
  try {
1907
- rows5 = (await db.query({
1952
+ rows6 = (await db.query({
1908
1953
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
1909
1954
  })).tiers;
1910
1955
  } catch {
1911
1956
  throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
1912
1957
  }
1913
- if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1914
- const list = rows5.map((row2) => rowToTier(row2, groupId));
1958
+ if (!Array.isArray(rows6)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1959
+ const list = rows6.map((row2) => rowToTier(row2, groupId));
1915
1960
  return resolveTiers(group, list);
1916
1961
  }
1917
1962
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -1940,10 +1985,14 @@ async function frozenCheckoutVariables(db, app) {
1940
1985
  if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
1941
1986
  formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
1942
1987
  }
1988
+ const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
1989
+ const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
1943
1990
  return {
1944
1991
  paymentAmount: formatted,
1945
1992
  paymentAmountSummary: formatted ? ` of ${formatted}` : "",
1946
- tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
1993
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
1994
+ namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
1995
+ namedSeatRecipientName
1947
1996
  };
1948
1997
  }
1949
1998
  async function catalogTierName(db, group, app) {
@@ -1972,8 +2021,8 @@ function formatInstant(at2, timeZone) {
1972
2021
  var adminUrlFor = (origin) => `${origin}/admin/`;
1973
2022
  var membersUrlFor = (origin) => `${origin}/members/`;
1974
2023
  function applicantVariables(app) {
1975
- const text10 = (v) => typeof v === "string" ? v : "";
1976
- return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
2024
+ const text11 = (v) => typeof v === "string" ? v : "";
2025
+ return { firstName: text11(app.firstName), lastName: text11(app.lastName), email: text11(app.email), phone: text11(app.phone), state: text11(app.state) };
1977
2026
  }
1978
2027
  function sampleVariables(origin, timeZone, now = Date.now()) {
1979
2028
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -2001,6 +2050,8 @@ function sampleVariables(origin, timeZone, now = Date.now()) {
2001
2050
  purchaserName: "Sample Purchaser",
2002
2051
  recipientName: "Sample Recipient",
2003
2052
  chapterName: "Sample Chapter",
2053
+ namedSeat: "paid",
2054
+ namedSeatRecipientName: "Sample Recipient",
2004
2055
  acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
2005
2056
  joinUrl: `${origin}/join?seat=sample`
2006
2057
  };
@@ -2017,8 +2068,8 @@ async function emailTierVariables(db, input) {
2017
2068
  if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
2018
2069
  const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
2019
2070
  if (!tierId) return { tierName: "", tier: "" };
2020
- 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() : "";
2071
+ const name2 = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
2072
+ const tierName2 = (0, import_crm7.isEmailTierName)(name2, tierId) ? name2.trim() : "";
2022
2073
  return { tierName: tierName2, tier: tierName2, tierId };
2023
2074
  } catch {
2024
2075
  return { tierName: "", tier: "" };
@@ -2026,8 +2077,8 @@ async function emailTierVariables(db, input) {
2026
2077
  }
2027
2078
 
2028
2079
  // src/notify.ts
2029
- function fromAddress(configured, name) {
2030
- const display = name?.trim();
2080
+ function fromAddress(configured, name2) {
2081
+ const display = name2?.trim();
2031
2082
  if (!display) return configured;
2032
2083
  const angled = configured.match(/<([^>]+)>/);
2033
2084
  return { name: display, email: (angled ? angled[1] : configured).trim() };
@@ -2078,7 +2129,7 @@ async function sendTemplated(deps, input) {
2078
2129
  ...input.applicationId ? { applicationId: input.applicationId } : {}
2079
2130
  };
2080
2131
  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" };
2132
+ if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
2082
2133
  try {
2083
2134
  const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
2084
2135
  mutationId: `email-claim:${id7}:${attempt}`,
@@ -2098,14 +2149,14 @@ async function sendTemplated(deps, input) {
2098
2149
  if (decision.transport === "log-only") return { sent: true };
2099
2150
  let messageId;
2100
2151
  try {
2101
- const text10 = String(row2.body);
2102
- const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text10);
2152
+ const text11 = String(row2.body);
2153
+ const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text11);
2103
2154
  const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
2104
2155
  const response = await deps.sender.send({
2105
2156
  from: fromAddress(deps.from, input.group.fromName),
2106
2157
  to: [decision.to],
2107
2158
  subject: String(row2.subject),
2108
- text: text10,
2159
+ text: text11,
2109
2160
  ...html ? { html } : {},
2110
2161
  ...headers ? { headers } : {},
2111
2162
  replyTo: input.group.replyTo
@@ -2385,13 +2436,13 @@ async function authoritativeState(db, input) {
2385
2436
  return state;
2386
2437
  }
2387
2438
  async function currentPolicy(db, input, restartAware, state) {
2388
- const rows5 = await db.query({
2439
+ const rows6 = await db.query({
2389
2440
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
2390
2441
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
2391
2442
  });
2392
- if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2393
- const offerings = rows5.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
2394
- const policies = rows5.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
2443
+ if (!Array.isArray(rows6.membershipOfferingVersion) || !Array.isArray(rows6.discountPolicyVersion) || rows6.membershipOfferingVersion.length >= 200 || rows6.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2444
+ const offerings = rows6.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
2445
+ const policies = rows6.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
2395
2446
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
2396
2447
  const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
2397
2448
  const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
@@ -2509,13 +2560,13 @@ async function membershipRestartPayment(db, target, member, now) {
2509
2560
  // src/checkout-named-seat.ts
2510
2561
  function checkoutNamedSeatSelection(value, purchaserEmail) {
2511
2562
  const row2 = value;
2512
- const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
2563
+ const name2 = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
2513
2564
  const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
2514
2565
  const purchaser = normalizeChapterEmail(purchaserEmail);
2515
- if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
2566
+ if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
2516
2567
  throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
2517
2568
  }
2518
- return { recipientName: name, recipientEmail: email };
2569
+ return { recipientName: name2, recipientEmail: email };
2519
2570
  }
2520
2571
  async function checkoutNamedSeatIdentity(target, email) {
2521
2572
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -2956,14 +3007,14 @@ async function readMembershipAccess(db, input, now) {
2956
3007
  const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
2957
3008
  requireMembershipScope(entitlement, input);
2958
3009
  const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
2959
- const allowed2 = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
3010
+ const allowed = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
2960
3011
  return {
2961
3012
  membershipId: id7,
2962
- allowed: allowed2,
3013
+ allowed,
2963
3014
  status: String(member.status),
2964
3015
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
2965
3016
  revision: Number(member.revision),
2966
- restartEligible: !allowed2 && membershipCanRestart(member, now)
3017
+ restartEligible: !allowed && membershipCanRestart(member, now)
2967
3018
  };
2968
3019
  }
2969
3020
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -3108,11 +3159,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
3108
3159
  if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
3109
3160
  throw new Error("Stripe returned an invalid commercial list response");
3110
3161
  }
3111
- const rows5 = page.data.map(asRaw).filter((row2) => row2 !== null);
3112
- if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3113
- objects.push(...rows5);
3162
+ const rows6 = page.data.map(asRaw).filter((row2) => row2 !== null);
3163
+ if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3164
+ objects.push(...rows6);
3114
3165
  if (!page.has_more) return { objects, complete: true };
3115
- const cursor = rows5.at(-1)?.id;
3166
+ const cursor = rows6.at(-1)?.id;
3116
3167
  if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
3117
3168
  startingAfter = cursor;
3118
3169
  }
@@ -3245,7 +3296,7 @@ async function approveApplication(input) {
3245
3296
  try {
3246
3297
  if (!duplicate) {
3247
3298
  const write = await db.transact(
3248
- [{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
3299
+ [{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvedAt: Date.now(), approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
3249
3300
  { mutationId: `approve:${id7}`, guards: [{
3250
3301
  ns: "applications",
3251
3302
  id: id7,
@@ -3528,8 +3579,8 @@ async function issueGiftClaim(db, intent, provider, now) {
3528
3579
  throw new Error("gift issuance conflict");
3529
3580
  }
3530
3581
  }
3531
- function buyerGiftClaims(rows5, now) {
3532
- return rows5.map((row2) => {
3582
+ function buyerGiftClaims(rows6, now) {
3583
+ return rows6.map((row2) => {
3533
3584
  const stored = String(row2.status ?? "pending");
3534
3585
  const status2 = stored === "pending" && Number(row2.codeExpiresAt) <= now ? "expired" : stored;
3535
3586
  return {
@@ -3675,8 +3726,8 @@ async function revokeGiftClaim(db, claim, reason, now) {
3675
3726
  // src/payments-route-support.ts
3676
3727
  async function findPaymentApplication(db, event2) {
3677
3728
  if ("applicationId" in event2 && event2.applicationId) {
3678
- const rows5 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3679
- return rows5.applications?.[0];
3729
+ const rows6 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3730
+ return rows6.applications?.[0];
3680
3731
  }
3681
3732
  if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
3682
3733
  const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
@@ -3867,6 +3918,306 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3867
3918
  return membershipFail("membership_dispute_retry_required");
3868
3919
  }
3869
3920
 
3921
+ // src/member-system-authority.ts
3922
+ var rows2 = (group) => group.fields.map((id7) => ({ ...group, id: id7 }));
3923
+ var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
3924
+ ...rows2({
3925
+ scope: "network",
3926
+ writer: "network_person",
3927
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3928
+ sensitivity: "pii",
3929
+ memberAccess: "edit",
3930
+ fields: [
3931
+ "person.legalName",
3932
+ "person.preferredName",
3933
+ "person.namePronunciation",
3934
+ "person.pronouns",
3935
+ "person.displayLegalName",
3936
+ "person.primaryEmail",
3937
+ "person.secondaryEmails",
3938
+ "person.phone",
3939
+ "person.linkedinUrl",
3940
+ "person.photo",
3941
+ "person.homeAddress",
3942
+ "person.fulfillmentFit",
3943
+ "person.fulfillmentSize"
3944
+ ]
3945
+ }),
3946
+ ...rows2({
3947
+ scope: "network",
3948
+ writer: "network_person",
3949
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3950
+ sensitivity: "ordinary",
3951
+ memberAccess: "edit",
3952
+ fields: [
3953
+ "person.title",
3954
+ "person.company",
3955
+ "person.companyObservedAt",
3956
+ "person.region",
3957
+ "person.whyText",
3958
+ "person.whyTokens",
3959
+ "person.skillsOffered",
3960
+ "person.openToRequests",
3961
+ "person.directoryPhotoConsent",
3962
+ "person.profilePrompts",
3963
+ "person.introducedByText"
3964
+ ]
3965
+ }),
3966
+ ...rows2({
3967
+ scope: "network",
3968
+ writer: "network_person",
3969
+ projections: ["bnf_admin", "chapter_admin"],
3970
+ sensitivity: "restricted",
3971
+ memberAccess: "none",
3972
+ fields: [
3973
+ "person.linkedinMemberId",
3974
+ "person.identityClaims",
3975
+ "person.aliases",
3976
+ "person.mergeHistory",
3977
+ "person.introducedByPersonId",
3978
+ "person.introducedAt",
3979
+ "person.firstTouch",
3980
+ "person.globalSuppression",
3981
+ "person.deceasedAt",
3982
+ "person.archivedAt"
3983
+ ]
3984
+ }),
3985
+ ...rows2({
3986
+ scope: "network",
3987
+ writer: "network_person",
3988
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3989
+ sensitivity: "sensitive",
3990
+ memberAccess: "edit",
3991
+ fields: ["person.accreditationSelfCertification", "person.accreditationCategory", "person.accreditationExpiresAt"]
3992
+ }),
3993
+ ...rows2({
3994
+ scope: "network",
3995
+ writer: "network_person",
3996
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3997
+ sensitivity: "restricted",
3998
+ memberAccess: "read",
3999
+ fields: ["person.acknowledgements"]
4000
+ }),
4001
+ ...rows2({
4002
+ scope: "provider",
4003
+ writer: "clerk",
4004
+ projections: ["clerk_identity", "bnf_admin", "chapter_admin"],
4005
+ sensitivity: "restricted",
4006
+ memberAccess: "none",
4007
+ fields: ["identity.clerkUserIds", "identity.clerkEmailVerification"]
4008
+ }),
4009
+ ...rows2({
4010
+ scope: "chapter",
4011
+ writer: "chapter_journey",
4012
+ projections: ["chapter_admin", "chapter_crm", "bnf_admin"],
4013
+ sensitivity: "ordinary",
4014
+ memberAccess: "none",
4015
+ fields: [
4016
+ "journey.chapterId",
4017
+ "journey.stage",
4018
+ "journey.stageEnteredAt",
4019
+ "journey.stageHistory",
4020
+ "journey.ending",
4021
+ "journey.endingReason",
4022
+ "journey.source",
4023
+ "journey.entryChapterId",
4024
+ "journey.route",
4025
+ "journey.flags",
4026
+ "journey.applicationStartedAt",
4027
+ "journey.appliedAt",
4028
+ "journey.paidAt",
4029
+ "journey.callBookedAt",
4030
+ "journey.metAt",
4031
+ "journey.approvedAt",
4032
+ "journey.onboardedAt",
4033
+ "journey.firstLoginAt",
4034
+ "journey.firstDealAt"
4035
+ ]
4036
+ }),
4037
+ ...rows2({
4038
+ scope: "chapter",
4039
+ writer: "chapter_journey",
4040
+ projections: ["chapter_admin", "chapter_crm"],
4041
+ sensitivity: "sensitive",
4042
+ memberAccess: "none",
4043
+ fields: [
4044
+ "journey.relationshipOwnerId",
4045
+ "journey.lastContactAt",
4046
+ "journey.nextAction",
4047
+ "journey.owes",
4048
+ "journey.asks",
4049
+ "journey.preparation",
4050
+ "journey.notes"
4051
+ ]
4052
+ }),
4053
+ ...rows2({
4054
+ scope: "chapter",
4055
+ writer: "chapter_journey",
4056
+ projections: ["chapter_admin", "chapter_crm", "member_self_service"],
4057
+ sensitivity: "ordinary",
4058
+ memberAccess: "edit",
4059
+ fields: ["journey.dealFlowConsent", "journey.directoryConsent", "journey.emailConsent", "journey.smsConsent"]
4060
+ }),
4061
+ ...rows2({
4062
+ scope: "chapter",
4063
+ writer: "chapter_journey",
4064
+ projections: ["chapter_admin", "bnf_admin", "member_self_service"],
4065
+ sensitivity: "ordinary",
4066
+ memberAccess: "read",
4067
+ fields: ["journey.memberNumber"]
4068
+ }),
4069
+ ...rows2({
4070
+ scope: "network",
4071
+ writer: "network_membership",
4072
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4073
+ sensitivity: "ordinary",
4074
+ memberAccess: "read",
4075
+ fields: [
4076
+ "membership.chapterId",
4077
+ "membership.journeyId",
4078
+ "membership.termStartsAt",
4079
+ "membership.termEndsAt",
4080
+ "membership.tier",
4081
+ "membership.status",
4082
+ "membership.joinedAt",
4083
+ "membership.autoRenews",
4084
+ "membership.renewalAt",
4085
+ "membership.cancelsAt",
4086
+ "membership.graceEndsAt",
4087
+ "membership.foundingCohort",
4088
+ "membership.offeringPolicyVersion",
4089
+ "membership.discountPolicyVersion"
4090
+ ]
4091
+ }),
4092
+ ...rows2({
4093
+ scope: "network",
4094
+ writer: "network_commercial_projection",
4095
+ projections: ["bnf_admin", "chapter_admin"],
4096
+ sensitivity: "restricted",
4097
+ memberAccess: "none",
4098
+ fields: [
4099
+ "membership.stripeCustomerId",
4100
+ "membership.stripeSubscriptionId",
4101
+ "membership.stripePriceId",
4102
+ "membership.lastInvoiceId",
4103
+ "membership.paymentOutcome",
4104
+ "membership.refundOutcome"
4105
+ ]
4106
+ }),
4107
+ ...rows2({
4108
+ scope: "network",
4109
+ writer: "network_discount_policy",
4110
+ projections: ["bnf_admin", "chapter_admin", "stripe_billing"],
4111
+ sensitivity: "ordinary",
4112
+ memberAccess: "none",
4113
+ fields: [
4114
+ "discountPolicy.chapterId",
4115
+ "discountPolicy.version",
4116
+ "discountPolicy.eligibleOfferingIds",
4117
+ "discountPolicy.percentOff",
4118
+ "discountPolicy.amountOff",
4119
+ "discountPolicy.activatesOn",
4120
+ "discountPolicy.invoiceCount",
4121
+ "discountPolicy.expiresAt",
4122
+ "discountPolicy.stackingOrder",
4123
+ "discountPolicy.status"
4124
+ ]
4125
+ }),
4126
+ ...rows2({
4127
+ scope: "network",
4128
+ writer: "network_entitlement",
4129
+ projections: ["bnf_admin", "chapter_admin", "member_self_service", "stripe_billing"],
4130
+ sensitivity: "ordinary",
4131
+ memberAccess: "read",
4132
+ fields: [
4133
+ "entitlement.membershipId",
4134
+ "entitlement.kind",
4135
+ "entitlement.priceCents",
4136
+ "entitlement.currency",
4137
+ "entitlement.payerPersonId",
4138
+ "entitlement.purchasedAt",
4139
+ "entitlement.termEndsAt",
4140
+ "entitlement.renewsWithMembership",
4141
+ "entitlement.status"
4142
+ ]
4143
+ }),
4144
+ ...rows2({
4145
+ scope: "network",
4146
+ writer: "network_seat",
4147
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4148
+ sensitivity: "pii",
4149
+ memberAccess: "read",
4150
+ fields: [
4151
+ "seat.membershipId",
4152
+ "seat.entitlementId",
4153
+ "seat.personId",
4154
+ "seat.recipientName",
4155
+ "seat.recipientEmail",
4156
+ "seat.role",
4157
+ "seat.status",
4158
+ "seat.acceptanceExpiresAt",
4159
+ "seat.acceptedAt",
4160
+ "seat.activatedAt",
4161
+ "seat.refundedAt"
4162
+ ]
4163
+ }),
4164
+ ...rows2({
4165
+ scope: "network",
4166
+ writer: "network_referral_ledger",
4167
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4168
+ sensitivity: "ordinary",
4169
+ memberAccess: "read",
4170
+ fields: [
4171
+ "referral.referrerPersonId",
4172
+ "referral.referredPersonId",
4173
+ "referral.qualifiedMembershipId",
4174
+ "referral.creditCents",
4175
+ "referral.status",
4176
+ "referral.appliedInvoiceId",
4177
+ "referral.carryForwardCents",
4178
+ "referral.adjustmentOf"
4179
+ ]
4180
+ }),
4181
+ ...rows2({
4182
+ scope: "network",
4183
+ writer: "network_investment_ledger",
4184
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4185
+ sensitivity: "restricted",
4186
+ memberAccess: "read",
4187
+ fields: [
4188
+ "investment.personId",
4189
+ "investment.chapterId",
4190
+ "investment.season",
4191
+ "investment.purpose",
4192
+ "investment.targetCount",
4193
+ "investment.status",
4194
+ "investment.amountCents",
4195
+ "investment.closedAt",
4196
+ "investment.sourceReference"
4197
+ ]
4198
+ }),
4199
+ ...rows2({
4200
+ scope: "provider",
4201
+ writer: "stripe",
4202
+ projections: ["stripe_billing", "bnf_admin", "chapter_admin"],
4203
+ sensitivity: "restricted",
4204
+ memberAccess: "none",
4205
+ fields: [
4206
+ "commercial.charge",
4207
+ "commercial.invoice",
4208
+ "commercial.refund",
4209
+ "commercial.providerSubscription",
4210
+ "commercial.paymentMethod"
4211
+ ]
4212
+ })
4213
+ ];
4214
+ var MEMBER_SELF_EDITABLE_FIELDS = new Set(
4215
+ MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess === "edit").map((row2) => row2.id)
4216
+ );
4217
+ var MEMBER_SELF_READABLE_FIELDS = new Set(
4218
+ MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
4219
+ );
4220
+
3870
4221
  // src/member-system-contract.ts
3871
4222
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3872
4223
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3878,8 +4229,8 @@ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink)
3878
4229
 
3879
4230
  // src/stripe-webhook-partition.ts
3880
4231
  var firstRow = async (db, ns, q) => {
3881
- const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
3882
- return Array.isArray(rows5) ? rows5[0] : void 0;
4232
+ const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
4233
+ return Array.isArray(rows6) ? rows6[0] : void 0;
3883
4234
  };
3884
4235
  var stripeEventIdentity = (event2) => ({
3885
4236
  ...event2.appId ? { appId: event2.appId } : {},
@@ -3986,8 +4337,8 @@ async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest,
3986
4337
  // src/stripe-event-reconcile.ts
3987
4338
  async function firstRow2(db, ns, q) {
3988
4339
  const res = await db.query({ [ns]: { $: q } });
3989
- const rows5 = res[ns];
3990
- return Array.isArray(rows5) ? rows5[0] : void 0;
4340
+ const rows6 = res[ns];
4341
+ return Array.isArray(rows6) ? rows6[0] : void 0;
3991
4342
  }
3992
4343
  async function reconcileChapterStripeEvent(input, env, ctx) {
3993
4344
  const db = ctx.makeDb(env);
@@ -4304,19 +4655,19 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
4304
4655
  const db = raw2, target = stripeEventTarget(env, ctx);
4305
4656
  const local4 = (row3) => row3.chapterId === target.chapterId && row3.appId === target.appId && row3.environment === target.environment && row3.runtime === target.runtime;
4306
4657
  if (req.method === "GET") {
4307
- const rows5 = (await db.query({ stripeEventReceipts: { $: {
4658
+ const rows6 = (await db.query({ stripeEventReceipts: { $: {
4308
4659
  where: { chapterId: target.chapterId, runtime: target.runtime },
4309
4660
  order: { receivedAt: "desc" },
4310
4661
  limit: 500
4311
4662
  } } })).stripeEventReceipts ?? [];
4312
- const pending = rows5.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
4663
+ const pending = rows6.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
4313
4664
  return json({ rows: pending.map((row3) => ({
4314
4665
  eventId: row3.eventId,
4315
4666
  kind: row3.kind,
4316
4667
  disposition: row3.disposition,
4317
4668
  receivedAt: row3.receivedAt,
4318
4669
  retryReady: row3.retryReady === true
4319
- })), truncated: rows5.length >= 500 });
4670
+ })), truncated: rows6.length >= 500 });
4320
4671
  }
4321
4672
  let eventId;
4322
4673
  try {
@@ -4502,9 +4853,9 @@ function memberApplication(app, meeting, defaultTimezone) {
4502
4853
  var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
4503
4854
  async function memberSessionApplication(db, chapterId, runtime, identity2) {
4504
4855
  const lookup = async (where) => {
4505
- const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4506
- if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
4507
- return rows5.filter((row2) => local(row2, chapterId, runtime));
4856
+ const rows6 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4857
+ if (!Array.isArray(rows6) || rows6.length >= 100) throw new Error("member identity lookup incomplete");
4858
+ return rows6.filter((row2) => local(row2, chapterId, runtime));
4508
4859
  };
4509
4860
  let apps = await lookup({ clerkUserId: identity2.userId });
4510
4861
  let emailFallback = false;
@@ -4881,10 +5232,10 @@ async function namedSeatRecipientClaimId(input, email) {
4881
5232
  }
4882
5233
  async function reserveNamedMembershipSeat(db, input, now) {
4883
5234
  membershipClock(now);
4884
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
5235
+ const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4885
5236
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4886
5237
  const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4887
- if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
5238
+ if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
4888
5239
  const state = await loadMembershipAuthorityState(db, input);
4889
5240
  const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4890
5241
  requireMembershipScope(member, input);
@@ -4901,7 +5252,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4901
5252
  id: id7,
4902
5253
  ...membershipScope(input),
4903
5254
  primaryMembershipId: memberId,
4904
- recipientName: name,
5255
+ recipientName: name2,
4905
5256
  recipientEmailNormalized: email,
4906
5257
  purchaserUserId: input.purchaserUserId,
4907
5258
  recipientClaimId: claimId,
@@ -4948,7 +5299,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4948
5299
  if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4949
5300
  }
4950
5301
  requireMembershipScope(seat, input);
4951
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
5302
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name2 || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4952
5303
  return seat;
4953
5304
  }
4954
5305
 
@@ -5139,9 +5490,9 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5139
5490
  membershipClock(now);
5140
5491
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
5141
5492
  const state = await loadMembershipAuthorityState(db, scope2);
5142
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5493
+ const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5143
5494
  let lifted = 0;
5144
- for (const member of rows5.slice(0, limit)) {
5495
+ for (const member of rows6.slice(0, limit)) {
5145
5496
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
5146
5497
  const offering = membershipOfferingVersion(member.offeringSnapshot);
5147
5498
  if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
@@ -5157,7 +5508,7 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5157
5508
  ] });
5158
5509
  lifted++;
5159
5510
  }
5160
- return { lifted, more: rows5.length > limit };
5511
+ return { lifted, more: rows6.length > limit };
5161
5512
  }
5162
5513
 
5163
5514
  // src/membership-seat-reprice.ts
@@ -5170,9 +5521,9 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
5170
5521
  const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
5171
5522
  if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
5172
5523
  const target = current.seatPriceCents;
5173
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5174
- const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
5175
- for (const member of rows5.slice(0, limit)) {
5524
+ const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5525
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows6.length > limit };
5526
+ for (const member of rows6.slice(0, limit)) {
5176
5527
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
5177
5528
  const id7 = String(member.id), skip = (reason) => {
5178
5529
  receipt.skipped.push({ membershipId: id7, reason });
@@ -5271,8 +5622,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
5271
5622
  }
5272
5623
  }
5273
5624
  if (!one) {
5274
- const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5275
- return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
5625
+ const rows6 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5626
+ return json({ seats: rows6.slice(0, 200).map(project), truncated: rows6.length > 200 });
5276
5627
  }
5277
5628
  const seat = await membershipLoad(db, "networkSeat", one[1]);
5278
5629
  if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
@@ -5491,8 +5842,8 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5491
5842
  const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
5492
5843
  const found = await db.query({ networkMembership: page, networkSeat: page });
5493
5844
  const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
5494
- const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5495
- const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5845
+ const rows6 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5846
+ const disputes = rows6.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5496
5847
  return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
5497
5848
  }
5498
5849
  const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
@@ -5542,9 +5893,9 @@ function stateOf(item, all, now, window, rivals) {
5542
5893
  var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
5543
5894
  var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
5544
5895
  var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
5545
- function normalize(rows5, collection, parse, invalid2) {
5896
+ function normalize(rows6, collection, parse, invalid2) {
5546
5897
  const out = [];
5547
- for (const row2 of rows5) {
5898
+ for (const row2 of rows6) {
5548
5899
  try {
5549
5900
  out.push({ value: parse(row2.snapshot), row: row2 });
5550
5901
  } catch (error) {
@@ -5555,11 +5906,11 @@ function normalize(rows5, collection, parse, invalid2) {
5555
5906
  }
5556
5907
  async function listMembershipPolicyVersions(db, input) {
5557
5908
  const where = input.chapterId ? { chapterId: input.chapterId } : {};
5558
- const rows5 = await db.query({
5909
+ const rows6 = await db.query({
5559
5910
  membershipOfferingVersion: { $: { where, limit: LIMIT } },
5560
5911
  discountPolicyVersion: { $: { where, limit: LIMIT } }
5561
5912
  });
5562
- const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
5913
+ const offeringRows = rows6.membershipOfferingVersion ?? [], policyRows = rows6.discountPolicyVersion ?? [];
5563
5914
  const invalid2 = [];
5564
5915
  const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
5565
5916
  const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
@@ -5699,22 +6050,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
5699
6050
  const skip = (membershipId, reason) => {
5700
6051
  receipt.skipped.push({ membershipId, reason });
5701
6052
  };
5702
- let rows5;
6053
+ let rows6;
5703
6054
  if (input.membershipIds) {
5704
- rows5 = [];
6055
+ rows6 = [];
5705
6056
  for (const id7 of input.membershipIds) {
5706
6057
  const member = await membershipLoad(db, "networkMembership", id7);
5707
6058
  if (!member) skip(id7, "not_found");
5708
6059
  else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
5709
- else rows5.push(member);
6060
+ else rows6.push(member);
5710
6061
  }
5711
6062
  } else {
5712
6063
  const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5713
6064
  if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
5714
6065
  receipt.more = found.length > limit;
5715
- rows5 = found.slice(0, limit);
6066
+ rows6 = found.slice(0, limit);
5716
6067
  }
5717
- for (const member of rows5) {
6068
+ for (const member of rows6) {
5718
6069
  const id7 = String(member.id);
5719
6070
  if (member.discountPolicyVersionId === policy2.id) {
5720
6071
  receipt.unchanged.push(id7);
@@ -6003,20 +6354,20 @@ function belongsToSignup(account, applicationId) {
6003
6354
  }
6004
6355
 
6005
6356
  // src/member.ts
6006
- var import_crm8 = require("@odla-ai/crm");
6357
+ var import_crm9 = require("@odla-ai/crm");
6007
6358
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
6008
6359
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
6009
6360
  var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
6010
6361
  function resolveApplication(a) {
6011
6362
  const required = a?.required ?? DEFAULT_REQUIRED;
6012
6363
  const optional = a?.optional ?? DEFAULT_OPTIONAL;
6013
- for (const [name, arr] of [["required", required], ["optional", optional]]) {
6364
+ for (const [name2, arr] of [["required", required], ["optional", optional]]) {
6014
6365
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
6015
- throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
6366
+ throw new Error(`defineChapter.application.${name2}: must be an array of field-name strings`);
6016
6367
  }
6017
6368
  const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
6018
6369
  if (unsafe) {
6019
- throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
6370
+ throw new Error(`defineChapter.application.${name2}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
6020
6371
  }
6021
6372
  }
6022
6373
  const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
@@ -6037,7 +6388,7 @@ function resolveApplication(a) {
6037
6388
  if (typeof expression !== "string" || !expression.trim()) {
6038
6389
  throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
6039
6390
  }
6040
- (0, import_crm8.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
6391
+ (0, import_crm9.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
6041
6392
  }
6042
6393
  }
6043
6394
  return {
@@ -6068,19 +6419,19 @@ function hasDisclaimerAck(fields) {
6068
6419
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
6069
6420
  function applicantProfile(chapter, fields) {
6070
6421
  const app = chapter.application;
6071
- const allowed2 = (f) => app.profileFields.includes(f);
6422
+ const allowed = (f) => app.profileFields.includes(f);
6072
6423
  const profile = {};
6073
6424
  for (const f of [...app.required, ...app.optional]) {
6074
- if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
6425
+ if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
6075
6426
  const v = fields[f];
6076
6427
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
6077
6428
  }
6078
- if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
6429
+ if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
6079
6430
  return Object.keys(profile).length > 0 ? profile : void 0;
6080
6431
  }
6081
6432
  async function submitApplication(db, chapter, fields, opts) {
6082
6433
  const app = chapter.application;
6083
- const fieldStates = (0, import_crm8.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
6434
+ const fieldStates = (0, import_crm9.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
6084
6435
  for (const [f, state] of Object.entries(fieldStates)) {
6085
6436
  if (!state.required) continue;
6086
6437
  const v = fields[f];
@@ -6194,10 +6545,10 @@ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
6194
6545
  applications: { $: { where: { emailNormalized: email }, limit: 10 } },
6195
6546
  $users: { $: { where: { email }, limit: 2 } }
6196
6547
  });
6197
- const rows5 = data.applications ?? [], others = rows5.filter((row2) => row2.id !== applicationId);
6548
+ const rows6 = data.applications ?? [], others = rows6.filter((row2) => row2.id !== applicationId);
6198
6549
  const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
6199
6550
  if (others.length !== closed.length) return conflict();
6200
- const original = rows5.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
6551
+ const original = rows6.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
6201
6552
  const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
6202
6553
  if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
6203
6554
  const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
@@ -6221,7 +6572,7 @@ async function rejectUnprovisionedSignup(db, applicationId, status2) {
6221
6572
  }
6222
6573
 
6223
6574
  // src/worker-routes.ts
6224
- var import_crm9 = require("@odla-ai/crm");
6575
+ var import_crm10 = require("@odla-ai/crm");
6225
6576
 
6226
6577
  // src/discussion-reference-security.ts
6227
6578
  async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
@@ -6321,15 +6672,15 @@ async function signupControlDigest(document) {
6321
6672
 
6322
6673
  // src/admission-store-read.ts
6323
6674
  async function readAdmissionRows(db, namespace, where, maximum) {
6324
- const rows5 = [];
6675
+ const rows6 = [];
6325
6676
  for (let offset = 0; offset <= maximum; offset += 500) {
6326
6677
  const result = await db.query({ [namespace]: { $: { where, order: { id: "asc" }, limit: 500, offset } } });
6327
6678
  const page = Array.isArray(result[namespace]) ? result[namespace] : [];
6328
- rows5.push(...page);
6329
- if (rows5.length > maximum) throw new Error("admission_assignment_limit");
6679
+ rows6.push(...page);
6680
+ if (rows6.length > maximum) throw new Error("admission_assignment_limit");
6330
6681
  if (page.length < 500) break;
6331
6682
  }
6332
- return rows5;
6683
+ return rows6;
6333
6684
  }
6334
6685
 
6335
6686
  // src/admission-grants.ts
@@ -6412,7 +6763,7 @@ async function createAdmissionGrantEnvelope(document) {
6412
6763
  if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
6413
6764
  return { document: checked.value, digest: await signupControlDigest(checked.value) };
6414
6765
  }
6415
- var rows2 = (value) => Array.isArray(value) ? value : [];
6766
+ var rows3 = (value) => Array.isArray(value) ? value : [];
6416
6767
  function asGrant(row2) {
6417
6768
  if (typeof row2.id !== "string" || typeof row2.emailNormalized !== "string") return null;
6418
6769
  if (row2.sourceScope !== "local" && row2.sourceScope !== "parent") return null;
@@ -6423,7 +6774,7 @@ async function findAdmissionGrant(db, targetId, email, now) {
6423
6774
  const emailNormalized = normalizeAdmissionEmail(email);
6424
6775
  if (!isAdmissionEmail(emailNormalized)) return null;
6425
6776
  const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
6426
- const active = rows2(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6777
+ const active = rows3(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6427
6778
  grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
6428
6779
  )).sort((a, b) => {
6429
6780
  const scope2 = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
@@ -6437,7 +6788,7 @@ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
6437
6788
  return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
6438
6789
  }
6439
6790
  const found = await db.query({ admissionGrantRevisions: {} });
6440
- return rows2(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
6791
+ return rows3(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
6441
6792
  }
6442
6793
  async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
6443
6794
  if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
@@ -6446,7 +6797,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
6446
6797
  order: { revision: "desc" },
6447
6798
  limit: 1
6448
6799
  } } });
6449
- const row2 = rows2(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
6800
+ const row2 = rows3(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
6450
6801
  return row2 ? row2 : null;
6451
6802
  }
6452
6803
  async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
@@ -6525,8 +6876,8 @@ var APPLICATION_LABELS = {
6525
6876
  state: "State"
6526
6877
  };
6527
6878
  function humanize(id7) {
6528
- const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6529
- return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
6879
+ const text11 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6880
+ return text11 ? `${text11[0].toUpperCase()}${text11.slice(1)}` : id7;
6530
6881
  }
6531
6882
  function applicationDefs(chapter) {
6532
6883
  try {
@@ -6619,8 +6970,8 @@ function validateGiftOfferCatalog(input, tierIds) {
6619
6970
  errors.push(`${path} must be an object`);
6620
6971
  continue;
6621
6972
  }
6622
- const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6623
- for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6973
+ const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6974
+ for (const key3 of Object.keys(item)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
6624
6975
  const id7 = text5(item.id, `${path}.id`, errors, 80);
6625
6976
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6626
6977
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
@@ -6684,27 +7035,27 @@ async function hasSignupControl(db, targetId, environment, active) {
6684
7035
  if (db.aggregate) {
6685
7036
  return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
6686
7037
  }
6687
- const rows5 = (await db.query({ signupControls: {} })).signupControls;
6688
- return Array.isArray(rows5) && rows5.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
7038
+ const rows6 = (await db.query({ signupControls: {} })).signupControls;
7039
+ return Array.isArray(rows6) && rows6.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
6689
7040
  }
6690
7041
  async function loadLatestSignupControl(db, targetId, environment) {
6691
7042
  if (!await hasSignupControl(db, targetId, environment)) return null;
6692
- const rows5 = (await db.query({ signupControls: { $: {
7043
+ const rows6 = (await db.query({ signupControls: { $: {
6693
7044
  where: { targetId, environment },
6694
7045
  order: { revision: "desc" },
6695
7046
  limit: 1
6696
7047
  } } })).signupControls;
6697
- const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
7048
+ const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6698
7049
  return asStored(latest);
6699
7050
  }
6700
7051
  async function loadActiveSignupControl(db, targetId, environment) {
6701
7052
  if (!await hasSignupControl(db, targetId, environment, true)) return null;
6702
- const rows5 = (await db.query({ signupControls: { $: {
7053
+ const rows6 = (await db.query({ signupControls: { $: {
6703
7054
  where: { targetId, environment, active: true },
6704
7055
  order: { revision: "desc" },
6705
7056
  limit: 1
6706
7057
  } } })).signupControls;
6707
- const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
7058
+ const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6708
7059
  return asStored(active);
6709
7060
  }
6710
7061
  function controlledApplication(base, document) {
@@ -7077,17 +7428,17 @@ async function readBoundedRequestText(req, maxBytes) {
7077
7428
  if (!reader) return "";
7078
7429
  const decoder = new TextDecoder();
7079
7430
  let bytes = 0;
7080
- let text10 = "";
7431
+ let text11 = "";
7081
7432
  for (; ; ) {
7082
7433
  const { done, value } = await reader.read();
7083
7434
  if (req.signal.aborted) return invalid2();
7084
7435
  if (done) {
7085
7436
  complete = true;
7086
- return text10 + decoder.decode();
7437
+ return text11 + decoder.decode();
7087
7438
  }
7088
7439
  bytes += value.byteLength;
7089
7440
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
7090
- text10 += decoder.decode(value, { stream: true });
7441
+ text11 += decoder.decode(value, { stream: true });
7091
7442
  }
7092
7443
  } catch {
7093
7444
  return invalid2();
@@ -7191,9 +7542,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
7191
7542
  let selection;
7192
7543
  if (request?.body) {
7193
7544
  try {
7194
- const text10 = await readBoundedRequestText(request, 2048);
7195
- if (text10 instanceof Response) return text10;
7196
- const body = JSON.parse(text10);
7545
+ const text11 = await readBoundedRequestText(request, 2048);
7546
+ if (text11 instanceof Response) return text11;
7547
+ const body = JSON.parse(text11);
7197
7548
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
7198
7549
  selection = body.namedSeat;
7199
7550
  } catch {
@@ -7346,9 +7697,9 @@ async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
7346
7697
  if (!app || app.groupId !== chapterId || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(runtime)) {
7347
7698
  throw new Error("invitation purchaser unavailable");
7348
7699
  }
7349
- const name = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7350
- if (!name) throw new Error("invitation purchaser name unavailable");
7351
- return name.slice(0, 160);
7700
+ const name2 = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7701
+ if (!name2) throw new Error("invitation purchaser name unavailable");
7702
+ return name2.slice(0, 160);
7352
7703
  }
7353
7704
  async function namedSeatApplicationContext(req, seatId, env, ctx) {
7354
7705
  const seats = ctx.membershipAuthority?.namedSeats;
@@ -7377,10 +7728,10 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
7377
7728
  }
7378
7729
  async function existingNamedSeatApplication(context, env, ctx) {
7379
7730
  const { db, userId, email, invitation } = context;
7380
- const rows5 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7381
- const apps = rows5.filter((row2) => !closedApplication(row2));
7731
+ const rows6 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7732
+ const apps = rows6.filter((row2) => !closedApplication(row2));
7382
7733
  if (!apps.length) {
7383
- const closed = rows5.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
7734
+ const closed = rows6.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
7384
7735
  return { predecessor: closed ? { id: String(closed.id) } : null };
7385
7736
  }
7386
7737
  const app = apps[0];
@@ -7745,6 +8096,111 @@ async function notifySubmitConfirmed(db, env, notice) {
7745
8096
  }
7746
8097
  }
7747
8098
 
8099
+ // src/member-entitlement.ts
8100
+ function entitledTierId(app, now = Date.now()) {
8101
+ const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
8102
+ const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
8103
+ if (granted && (endsAt === null || endsAt > now)) return granted;
8104
+ return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
8105
+ }
8106
+
8107
+ // src/portal-member.ts
8108
+ var text6 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8109
+ async function activeHubGrant(db, identity2) {
8110
+ const byUser = (await db.query({ hubMemberGrants: { $: { where: { userId: identity2.userId }, limit: 5 } } })).hubMemberGrants ?? [];
8111
+ const byEmail = identity2.email ? (await db.query({ hubMemberGrants: { $: { where: { emailNormalized: normalizeChapterEmail(identity2.email) }, limit: 1 } } })).hubMemberGrants ?? [] : [];
8112
+ return [...byUser, ...byEmail].find((row2) => row2.status === "active") ?? null;
8113
+ }
8114
+ async function resolvePortalMember(db, env, ctx, identity2) {
8115
+ try {
8116
+ if (ctx.chapter.mode === "hub") {
8117
+ const grant = await activeHubGrant(db, identity2);
8118
+ if (!grant || !identity2.email) return { status: "not-member" };
8119
+ return { status: "member", member: {
8120
+ memberId: String(grant.id),
8121
+ source: "hub-grant",
8122
+ email: identity2.email,
8123
+ ...text6(grant.firstName) ? { firstName: text6(grant.firstName) } : {},
8124
+ ...text6(grant.lastName) ? { lastName: text6(grant.lastName) } : {},
8125
+ tierId: null,
8126
+ memberSince: Number(grant.grantedAt) || null,
8127
+ row: grant
8128
+ } };
8129
+ }
8130
+ const session = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, identity2);
8131
+ if (!session) return { status: "not-member" };
8132
+ const app = session.source;
8133
+ if (!await chapterMembershipAccess(db, env, ctx, app)) return { status: "not-member", row: app };
8134
+ return { status: "member", member: {
8135
+ memberId: String(app.id),
8136
+ source: "application",
8137
+ email: String(app.email ?? identity2.email ?? ""),
8138
+ ...text6(app.firstName) ? { firstName: text6(app.firstName) } : {},
8139
+ ...text6(app.lastName) ? { lastName: text6(app.lastName) } : {},
8140
+ tierId: entitledTierId(app),
8141
+ memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
8142
+ row: app
8143
+ } };
8144
+ } catch {
8145
+ return { status: "unavailable" };
8146
+ }
8147
+ }
8148
+ function initialsOf(name2) {
8149
+ const words2 = name2.trim().split(/\s+/).filter(Boolean);
8150
+ const letters = words2.length > 1 ? [words2[0], words2[words2.length - 1]] : words2;
8151
+ return letters.map((word) => [...word][0].toUpperCase()).join("") || "?";
8152
+ }
8153
+ async function portalIdentity(db, ctx, member) {
8154
+ const profile = (await db.query({ memberProfiles: { $: { where: { id: member.memberId }, limit: 1 } } })).memberProfiles?.[0];
8155
+ let tier = null;
8156
+ if (member.tierId) {
8157
+ const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
8158
+ const match = group ? (await loadTiers(db, group)).find((candidate) => candidate.id === member.tierId) : void 0;
8159
+ tier = { id: member.tierId, name: match?.name ?? member.tierId };
8160
+ }
8161
+ const legal = [member.firstName, member.lastName].filter(Boolean).join(" ");
8162
+ const displayName = text6(profile?.preferredName) ?? (legal || member.email);
8163
+ const photoPath = text6(profile?.photoPath);
8164
+ const photoUrl = photoPath && db.storage && ctx.chapter.memberPortal?.photos ? await db.storage.sign(photoPath, 3600).catch(() => null) : null;
8165
+ return {
8166
+ memberId: member.memberId,
8167
+ tier,
8168
+ memberSince: member.memberSince,
8169
+ displayName,
8170
+ firstName: text6(profile?.preferredName)?.split(/\s+/)[0] ?? member.firstName ?? null,
8171
+ initials: initialsOf(displayName),
8172
+ location: text6(profile?.location) ?? null,
8173
+ photoUrl
8174
+ };
8175
+ }
8176
+
8177
+ // src/portal-me.ts
8178
+ var servesMembers = (ctx) => ctx.chapter.mode === "chapter" || Boolean(ctx.chapter.memberPortal || ctx.chapter.memberContent);
8179
+ async function hubMe(db, env, ctx, identity2, base) {
8180
+ if (!servesMembers(ctx)) return base;
8181
+ const result = await resolvePortalMember(db, env, ctx, identity2);
8182
+ if (result.status === "unavailable") return { ...base, memberAccess: null, portal: null };
8183
+ if (result.status !== "member") return { ...base, memberAccess: false, portal: null };
8184
+ return { ...base, memberAccess: true, portal: await identityOrNull(db, ctx, result.member) };
8185
+ }
8186
+ async function chapterPortalIdentity(db, ctx, memberAccess, app) {
8187
+ if (!memberAccess || !app || !ctx.chapter.memberPortal) return null;
8188
+ const text11 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8189
+ return identityOrNull(db, ctx, {
8190
+ memberId: String(app.id),
8191
+ source: "application",
8192
+ email: String(app.email ?? ""),
8193
+ ...text11(app.firstName) ? { firstName: text11(app.firstName) } : {},
8194
+ ...text11(app.lastName) ? { lastName: text11(app.lastName) } : {},
8195
+ tierId: entitledTierId(app),
8196
+ memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
8197
+ row: app
8198
+ });
8199
+ }
8200
+ async function identityOrNull(db, ctx, member) {
8201
+ return portalIdentity(db, ctx, member).catch(() => null);
8202
+ }
8203
+
7748
8204
  // src/worker-routes-health.ts
7749
8205
  var handleConfig = async (_req, url, env, ctx) => {
7750
8206
  if (url.pathname !== "/api/config") return null;
@@ -7759,13 +8215,13 @@ var handleHealth = async (_req, url, env, ctx) => {
7759
8215
  if (url.pathname !== "/api/health") return null;
7760
8216
  const response = json({ ok: true });
7761
8217
  response.headers.set("cache-control", "no-store");
7762
- for (const [name, value] of Object.entries({
8218
+ for (const [name2, value] of Object.entries({
7763
8219
  "x-odla-app-id": env.ODLA_APP_ID,
7764
8220
  "x-odla-chapter-id": ctx.chapter.id,
7765
8221
  "x-odla-data-environment": env.ODLA_ENV,
7766
8222
  "x-odla-runtime": env.ODLA_RUNTIME || "default"
7767
8223
  })) {
7768
- if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name, value);
8224
+ if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name2, value);
7769
8225
  }
7770
8226
  return response;
7771
8227
  };
@@ -7779,8 +8235,10 @@ var handleMe = async (req, url, env, ctx) => {
7779
8235
  const db = ctx.makeDb(env);
7780
8236
  const role = await ctx.roleFor(db, u);
7781
8237
  const superAdmin = await ctx.isSuperAdminEmail(db, u.email, u.userId);
7782
- const base = { authorized: superAdmin || isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
7783
- if (ctx.chapter.mode !== "chapter") return json(base);
8238
+ const admin = superAdmin || isAdminRole(role, ctx.auth);
8239
+ const secondFactorOk = !admin || adminSecondFactorAllows(ctx.auth.adminSecondFactor ?? "off", u.payload, u.email);
8240
+ const base = { authorized: admin && secondFactorOk, role, superAdmin, email: u.email ?? null, ...admin && !secondFactorOk ? { secondFactorRequired: true } : {} };
8241
+ if (ctx.chapter.mode !== "chapter") return json(await hubMe(db, env, ctx, u, base));
7784
8242
  let member;
7785
8243
  try {
7786
8244
  member = await memberSessionApplication(
@@ -7816,13 +8274,14 @@ var handleMe = async (req, url, env, ctx) => {
7816
8274
  } catch {
7817
8275
  }
7818
8276
  }
7819
- return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
8277
+ const portal = await chapterPortalIdentity(db, ctx, memberAccess, member?.source);
8278
+ return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, portal, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
7820
8279
  };
7821
8280
  var handleCrm = async (req, url, env, ctx) => {
7822
8281
  const crmBase = ctx.crmBase;
7823
8282
  if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + "/")) return null;
7824
8283
  const db = ctx.makeDb(env);
7825
- const routes = (0, import_crm9.createCrmRoutes)({
8284
+ const routes = (0, import_crm10.createCrmRoutes)({
7826
8285
  crm: ctx.chapter.crm,
7827
8286
  db,
7828
8287
  authorize: async (r) => {
@@ -8061,9 +8520,9 @@ var MAX_BENEFITS = 24;
8061
8520
  var MAX_MARKDOWN = 65536;
8062
8521
  var MAX_LABEL = 160;
8063
8522
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8064
- function rejectUnknownKeys(value, allowed2, path, errors) {
8523
+ function rejectUnknownKeys(value, allowed, path, errors) {
8065
8524
  for (const key3 of Object.keys(value)) {
8066
- if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8525
+ if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8067
8526
  }
8068
8527
  }
8069
8528
  function boundedString(value, path, errors, max, required = false) {
@@ -8075,10 +8534,10 @@ function boundedString(value, path, errors, max, required = false) {
8075
8534
  errors.push(`${path} must be a string`);
8076
8535
  return void 0;
8077
8536
  }
8078
- const text10 = value.trim();
8079
- if (required && !text10) errors.push(`${path} is required`);
8080
- if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8081
- return text10;
8537
+ const text11 = value.trim();
8538
+ if (required && !text11) errors.push(`${path} is required`);
8539
+ if (text11.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8540
+ return text11;
8082
8541
  }
8083
8542
  function validateMembershipPageDocument(input, options = {}) {
8084
8543
  const errors = [];
@@ -8127,8 +8586,8 @@ function validateMembershipPageDocument(input, options = {}) {
8127
8586
  else {
8128
8587
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
8129
8588
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
8130
- const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8131
- return text10 ? [text10] : [];
8589
+ const text11 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8590
+ return text11 ? [text11] : [];
8132
8591
  });
8133
8592
  }
8134
8593
  }
@@ -8219,10 +8678,10 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
8219
8678
  ]);
8220
8679
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
8221
8680
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8222
- function unknownKeys(value, allowed2, path, errors) {
8223
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8681
+ function unknownKeys(value, allowed, path, errors) {
8682
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8224
8683
  }
8225
- function text6(value, path, errors, max = 65536) {
8684
+ function text7(value, path, errors, max = 65536) {
8226
8685
  if (typeof value !== "string" || !value.trim()) {
8227
8686
  errors.push(`${path} is required`);
8228
8687
  return "";
@@ -8245,11 +8704,11 @@ function tiersOf(value, errors) {
8245
8704
  return [];
8246
8705
  }
8247
8706
  unknownKeys(item, TIER_KEYS2, path, errors);
8248
- const id7 = text6(item.id, `${path}.id`, errors, 80);
8707
+ const id7 = text7(item.id, `${path}.id`, errors, 80);
8249
8708
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8250
8709
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
8251
8710
  seen.add(id7);
8252
- const name = text6(item.name, `${path}.name`, errors, 160);
8711
+ const name2 = text7(item.name, `${path}.name`, errors, 160);
8253
8712
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
8254
8713
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
8255
8714
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -8267,10 +8726,10 @@ function tiersOf(value, errors) {
8267
8726
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
8268
8727
  errors.push(`${path}.interval must be year or month`);
8269
8728
  }
8270
- const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
8729
+ const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
8271
8730
  return [{
8272
8731
  id: id7,
8273
- name,
8732
+ name: name2,
8274
8733
  available: item.available === true,
8275
8734
  sortOrder: Number(item.sortOrder) || 0,
8276
8735
  priceCents: Number(item.priceCents) || 0,
@@ -8313,7 +8772,7 @@ function policyOf(value, errors, tiers) {
8313
8772
  errors.push("document.policy must be an object");
8314
8773
  return void 0;
8315
8774
  }
8316
- const allowed2 = /* @__PURE__ */ new Set([
8775
+ const allowed = /* @__PURE__ */ new Set([
8317
8776
  "disclaimerText",
8318
8777
  "refundPolicyText",
8319
8778
  "merchantDisclosureText",
@@ -8321,11 +8780,11 @@ function policyOf(value, errors, tiers) {
8321
8780
  "commitmentText",
8322
8781
  "normsText"
8323
8782
  ]);
8324
- unknownKeys(input, /* @__PURE__ */ new Set([...allowed2, "tierPolicies"]), "document.policy", errors);
8783
+ unknownKeys(input, /* @__PURE__ */ new Set([...allowed, "tierPolicies"]), "document.policy", errors);
8325
8784
  const result = {};
8326
- for (const key3 of allowed2) {
8785
+ for (const key3 of allowed) {
8327
8786
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
8328
- result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
8787
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
8329
8788
  }
8330
8789
  const tierPolicies = {};
8331
8790
  if (input.tierPolicies !== void 0) {
@@ -8340,8 +8799,8 @@ function policyOf(value, errors, tiers) {
8340
8799
  }
8341
8800
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
8342
8801
  tierPolicies[id7] = {
8343
- refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8344
- ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8802
+ refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8803
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8345
8804
  };
8346
8805
  }
8347
8806
  }
@@ -8381,9 +8840,9 @@ function validateSignupControlDocument(input) {
8381
8840
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
8382
8841
  const source = recordOf(root.source);
8383
8842
  const target = recordOf(root.target);
8384
- const appId = text6(source?.appId, "document.source.appId", errors, 63);
8385
- const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
8386
- const environment = text6(target?.environment, "document.target.environment", errors, 63);
8843
+ const appId = text7(source?.appId, "document.source.appId", errors, 63);
8844
+ const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
8845
+ const environment = text7(target?.environment, "document.target.environment", errors, 63);
8387
8846
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
8388
8847
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
8389
8848
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -8524,14 +8983,45 @@ async function memberContentDigest(document) {
8524
8983
  return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
8525
8984
  }
8526
8985
 
8986
+ // src/member-content-kinds.ts
8987
+ var MEMBER_PAGE_FEATURES = ["course", "share"];
8988
+ var MEMBER_PAGE_ICONS = [
8989
+ "home",
8990
+ "course",
8991
+ "library",
8992
+ "deals",
8993
+ "training",
8994
+ "events",
8995
+ "members",
8996
+ "share",
8997
+ "profile",
8998
+ "document",
8999
+ "calendar",
9000
+ "chat",
9001
+ "star"
9002
+ ];
9003
+ var MEMBER_PAGE_TAGS = ["new"];
9004
+ function composeMemberSections(parent, chapter, pages) {
9005
+ const parentDeclared = new Map((parent?.sections ?? []).map((section) => [section.id, section]));
9006
+ const out = /* @__PURE__ */ new Map();
9007
+ for (const section of chapter?.sections ?? []) {
9008
+ const label = section.label ?? parentDeclared.get(section.id)?.label;
9009
+ out.set(section.id, { id: section.id, ...label ? { label } : {} });
9010
+ }
9011
+ for (const section of parent?.sections ?? []) if (!out.has(section.id)) out.set(section.id, section);
9012
+ const named = [...new Set(pages.map((page) => page.navigation.section))];
9013
+ for (const id7 of named.filter((id8) => !out.has(id8)).sort((a, b) => a.localeCompare(b))) out.set(id7, { id: id7, label: id7 });
9014
+ return [...out.values()].filter((section) => named.includes(section.id));
9015
+ }
9016
+
8527
9017
  // src/member-content-validation-helpers.ts
8528
9018
  var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
8529
9019
  var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8530
- var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
9020
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides", "sections"]);
8531
9021
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
8532
9022
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
8533
- var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
8534
- var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
9023
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell", "kind", "placeholder", "feature"]);
9024
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId", "tag", "icon"]);
8535
9025
  var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
8536
9026
  var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
8537
9027
  var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
@@ -8540,8 +9030,8 @@ var MAX_PAGES = 100;
8540
9030
  var MAX_MARKDOWN2 = 65536;
8541
9031
  var MAX_TEXT = 240;
8542
9032
  var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8543
- function unknownKeys2(value, allowed2, path, errors) {
8544
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
9033
+ function unknownKeys2(value, allowed, path, errors) {
9034
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8545
9035
  }
8546
9036
  function textValue(value, path, errors, max = MAX_TEXT, required = false) {
8547
9037
  if (value === void 0) {
@@ -8581,13 +9071,140 @@ function offerValue(value, path, errors, tiers, audience) {
8581
9071
  };
8582
9072
  }
8583
9073
 
9074
+ // src/member-content-validation-v2.ts
9075
+ var SECTION_KEYS = /* @__PURE__ */ new Set(["id", "label"]);
9076
+ var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set(["availableYear", "badge", "actionLabel"]);
9077
+ var FEATURE_KEYS = /* @__PURE__ */ new Set(["name", "ref"]);
9078
+ var MAX_SECTIONS = 24;
9079
+ var V2_ROOT_KEYS = ["sections"];
9080
+ var V2_PAGE_KEYS = ["kind", "placeholder", "feature"];
9081
+ var V2_NAV_KEYS = ["tag", "icon"];
9082
+ function rejectV2Fields(raw2, keys3, path, errors) {
9083
+ for (const key3 of keys3) if (raw2[key3] !== void 0) errors.push(`${path}.${key3} requires schemaVersion 2`);
9084
+ }
9085
+ function sectionsValue(value, errors) {
9086
+ if (value === void 0) return void 0;
9087
+ if (!Array.isArray(value)) {
9088
+ errors.push("document.sections must be an array");
9089
+ return void 0;
9090
+ }
9091
+ if (value.length > MAX_SECTIONS) errors.push(`document.sections supports at most ${MAX_SECTIONS} entries`);
9092
+ const seen = /* @__PURE__ */ new Set();
9093
+ const sections = [];
9094
+ for (const [index2, item] of value.slice(0, MAX_SECTIONS).entries()) {
9095
+ const path = `document.sections[${index2}]`;
9096
+ const raw2 = record2(item);
9097
+ if (!raw2) {
9098
+ errors.push(`${path} must be an object`);
9099
+ continue;
9100
+ }
9101
+ unknownKeys2(raw2, SECTION_KEYS, path, errors);
9102
+ const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
9103
+ if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9104
+ if (seen.has(id7)) errors.push(`${path}.id duplicates section "${id7}"`);
9105
+ seen.add(id7);
9106
+ const label = textValue(raw2.label, `${path}.label`, errors, 120);
9107
+ sections.push({ id: id7, ...label ? { label } : {} });
9108
+ }
9109
+ return sections;
9110
+ }
9111
+ var oneOf = (values, value) => typeof value === "string" && values.includes(value);
9112
+ function navigationV2(raw2, path, errors) {
9113
+ const out = {};
9114
+ if (raw2.tag !== void 0) {
9115
+ if (oneOf(MEMBER_PAGE_TAGS, raw2.tag)) out.tag = raw2.tag;
9116
+ else errors.push(`${path}.tag must be one of ${MEMBER_PAGE_TAGS.join(", ")}`);
9117
+ }
9118
+ if (raw2.icon !== void 0) {
9119
+ if (oneOf(MEMBER_PAGE_ICONS, raw2.icon)) out.icon = raw2.icon;
9120
+ else errors.push(`${path}.icon must be one of ${MEMBER_PAGE_ICONS.join(", ")}`);
9121
+ }
9122
+ return out;
9123
+ }
9124
+ function placeholderValue(value, path, errors) {
9125
+ const raw2 = record2(value);
9126
+ if (!raw2) {
9127
+ errors.push(`${path} must be an object`);
9128
+ return {};
9129
+ }
9130
+ unknownKeys2(raw2, PLACEHOLDER_KEYS, path, errors);
9131
+ const year = raw2.availableYear;
9132
+ if (year !== void 0 && (!Number.isInteger(year) || Number(year) < 2e3 || Number(year) > 2100)) {
9133
+ errors.push(`${path}.availableYear must be a year from 2000 to 2100`);
9134
+ }
9135
+ const badge = textValue(raw2.badge, `${path}.badge`, errors, 60);
9136
+ const actionLabel = textValue(raw2.actionLabel, `${path}.actionLabel`, errors, 80);
9137
+ return {
9138
+ ...Number.isInteger(year) ? { availableYear: Number(year) } : {},
9139
+ ...badge ? { badge } : {},
9140
+ ...actionLabel ? { actionLabel } : {}
9141
+ };
9142
+ }
9143
+ function featureValue(value, path, errors) {
9144
+ const raw2 = record2(value);
9145
+ if (!raw2) {
9146
+ errors.push(`${path} must be an object`);
9147
+ return void 0;
9148
+ }
9149
+ unknownKeys2(raw2, FEATURE_KEYS, path, errors);
9150
+ if (!oneOf(MEMBER_PAGE_FEATURES, raw2.name)) {
9151
+ errors.push(`${path}.name must be one of ${MEMBER_PAGE_FEATURES.join(", ")}`);
9152
+ return void 0;
9153
+ }
9154
+ const ref = textValue(raw2.ref, `${path}.ref`, errors, 80);
9155
+ if (ref && !SLUG2.test(ref)) errors.push(`${path}.ref must be a lowercase slug`);
9156
+ return { name: raw2.name, ...ref ? { ref } : {} };
9157
+ }
9158
+ function pageKindValue(raw2, path, errors) {
9159
+ const kind = raw2.kind ?? "markdown";
9160
+ if (kind !== "markdown" && kind !== "placeholder" && kind !== "feature") {
9161
+ errors.push(`${path}.kind must be markdown, placeholder or feature`);
9162
+ return {};
9163
+ }
9164
+ if (kind !== "placeholder" && raw2.placeholder !== void 0) errors.push(`${path}.placeholder requires kind placeholder`);
9165
+ if (kind !== "feature" && raw2.feature !== void 0) errors.push(`${path}.feature requires kind feature`);
9166
+ if (kind === "markdown" && raw2.bodyMarkdown === void 0) errors.push(`${path}.bodyMarkdown is required`);
9167
+ if (kind === "feature" && raw2.bodyMarkdown !== void 0) errors.push(`${path}.bodyMarkdown is not supported on a feature page`);
9168
+ const out = raw2.kind === void 0 ? {} : { kind };
9169
+ if (kind === "placeholder") out.placeholder = placeholderValue(raw2.placeholder ?? {}, `${path}.placeholder`, errors);
9170
+ if (kind === "feature") {
9171
+ if (raw2.feature === void 0) errors.push(`${path}.feature is required for kind feature`);
9172
+ else {
9173
+ const feature = featureValue(raw2.feature, `${path}.feature`, errors);
9174
+ if (feature) out.feature = feature;
9175
+ }
9176
+ }
9177
+ return out;
9178
+ }
9179
+ function duplicateFeatures(pages, errors) {
9180
+ const seen = /* @__PURE__ */ new Set();
9181
+ for (const [index2, page] of pages.entries()) {
9182
+ if (!page.feature) continue;
9183
+ const key3 = `${page.feature.name}:${page.feature.ref ?? ""}`;
9184
+ if (seen.has(key3)) errors.push(`document.pages[${index2}].feature duplicates ${page.feature.name}${page.feature.ref ? ` "${page.feature.ref}"` : ""}`);
9185
+ seen.add(key3);
9186
+ }
9187
+ }
9188
+ function sectionReferences(pages, sections, parentPages, errors) {
9189
+ if (!sections) return;
9190
+ const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), ...parentPages.map((page) => page.navigation.section)]);
9191
+ for (const [index2, page] of pages.entries()) {
9192
+ if (!known.has(page.navigation.section)) {
9193
+ errors.push(`document.pages[${index2}].navigation.section references undeclared section "${page.navigation.section}"`);
9194
+ }
9195
+ }
9196
+ }
9197
+
8584
9198
  // src/member-content-validation.ts
8585
9199
  function validateMemberContentDocument(input, options = {}) {
8586
9200
  const errors = [];
8587
9201
  const root = record2(input);
8588
9202
  if (!root) return { ok: false, errors: ["document must be an object"] };
8589
9203
  unknownKeys2(root, ROOT_KEYS3, "document", errors);
8590
- if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
9204
+ if (root.schemaVersion !== 1 && root.schemaVersion !== 2) errors.push("document.schemaVersion must be 1 or 2");
9205
+ const v2 = root.schemaVersion === 2;
9206
+ if (!v2) rejectV2Fields(root, V2_ROOT_KEYS, "document", errors);
9207
+ const sections = v2 ? sectionsValue(root.sections, errors) : void 0;
8591
9208
  if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
8592
9209
  if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
8593
9210
  const sourceRaw = record2(root.source);
@@ -8632,6 +9249,8 @@ function validateMemberContentDocument(input, options = {}) {
8632
9249
  continue;
8633
9250
  }
8634
9251
  unknownKeys2(raw2, PAGE_KEYS, path, errors);
9252
+ if (!v2) rejectV2Fields(raw2, V2_PAGE_KEYS, path, errors);
9253
+ const kindFields = v2 ? pageKindValue(raw2, path, errors) : {};
8635
9254
  const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
8636
9255
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
8637
9256
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
@@ -8646,13 +9265,19 @@ function validateMemberContentDocument(input, options = {}) {
8646
9265
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
8647
9266
  else {
8648
9267
  unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
9268
+ if (!v2) rejectV2Fields(navRaw, V2_NAV_KEYS, `${path}.navigation`, errors);
8649
9269
  const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
8650
9270
  const order2 = navRaw.order;
8651
9271
  if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
8652
9272
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
8653
9273
  }
8654
9274
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
8655
- navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
9275
+ navigation = {
9276
+ section,
9277
+ order: Number.isInteger(order2) ? Number(order2) : index2,
9278
+ ...parentId ? { parentId } : {},
9279
+ ...v2 ? navigationV2(navRaw, `${path}.navigation`, errors) : {}
9280
+ };
8656
9281
  }
8657
9282
  const audienceRaw = record2(raw2.audience);
8658
9283
  let audience = { kind: "all-members" };
@@ -8678,7 +9303,7 @@ function validateMemberContentDocument(input, options = {}) {
8678
9303
  }
8679
9304
  const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
8680
9305
  const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
8681
- const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
9306
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, !v2);
8682
9307
  let upsell;
8683
9308
  if (raw2.upsell !== void 0) {
8684
9309
  const upsellRaw = record2(raw2.upsell);
@@ -8695,7 +9320,22 @@ function validateMemberContentDocument(input, options = {}) {
8695
9320
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
8696
9321
  }
8697
9322
  }
8698
- pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
9323
+ pages.push({
9324
+ id: id7,
9325
+ slug,
9326
+ title,
9327
+ ...summary ? { summary } : {},
9328
+ navigation,
9329
+ audience,
9330
+ ...teaserMarkdown ? { teaserMarkdown } : {},
9331
+ ...bodyMarkdown !== void 0 ? { bodyMarkdown } : {},
9332
+ ...upsell ? { upsell } : {},
9333
+ ...kindFields
9334
+ });
9335
+ }
9336
+ if (v2) {
9337
+ duplicateFeatures(pages, errors);
9338
+ sectionReferences(pages, sections, options.parentPages ?? [], errors);
8699
9339
  }
8700
9340
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
8701
9341
  for (const [index2, page] of pages.entries()) {
@@ -8742,21 +9382,21 @@ function validateMemberContentDocument(input, options = {}) {
8742
9382
  }
8743
9383
  if (errors.length) return { ok: false, errors };
8744
9384
  return { ok: true, value: {
8745
- schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
9385
+ schemaVersion: v2 ? 2 : 1,
8746
9386
  revision: Number(root.revision),
8747
9387
  generatedAt: Number(root.generatedAt),
8748
9388
  source,
8749
9389
  ...target ? { target } : {},
8750
9390
  pages,
8751
- ...upsellOverrides ? { upsellOverrides } : {}
9391
+ ...upsellOverrides ? { upsellOverrides } : {},
9392
+ ...sections ? { sections } : {}
8752
9393
  } };
8753
9394
  }
8754
9395
 
8755
9396
  // src/member-content.ts
8756
- var MEMBER_CONTENT_SCHEMA_VERSION = 1;
8757
9397
  var MEMBER_CONTENT_PATH = "/api/member-content";
8758
9398
  var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
8759
- var pageOrder = (a, b) => a.navigation.section.localeCompare(b.navigation.section) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
9399
+ var pageOrder = (rank) => (a, b) => (rank.get(a.navigation.section) ?? 0) - (rank.get(b.navigation.section) ?? 0) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
8760
9400
  function composeMemberContent(parent, chapter) {
8761
9401
  const pages = [];
8762
9402
  const parentPages = /* @__PURE__ */ new Map();
@@ -8775,20 +9415,24 @@ function composeMemberContent(parent, chapter) {
8775
9415
  }
8776
9416
  pages.push({ ...page, source: chapter.source });
8777
9417
  }
9418
+ const sections = composeMemberSections(parent, chapter, pages);
9419
+ const rank = new Map(sections.map((section, index2) => [section.id, index2]));
8778
9420
  return {
8779
- pages: pages.sort(pageOrder),
9421
+ pages: pages.sort(pageOrder(rank)),
9422
+ sections,
8780
9423
  parentRevision: parent?.revision ?? null,
8781
9424
  chapterRevision: chapter?.revision ?? null
8782
9425
  };
8783
9426
  }
8784
- var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
9427
+ var memberPageAllowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
8785
9428
  function projectMemberContent(content, viewer, tiers) {
8786
9429
  const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
8787
9430
  return {
8788
9431
  viewer: { ...viewer },
8789
9432
  revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
9433
+ sections: content.sections ?? composeMemberSections(null, null, content.pages),
8790
9434
  pages: content.pages.map((page) => {
8791
- const canRead = allowed(page, viewer);
9435
+ const canRead = memberPageAllowed(page, viewer);
8792
9436
  const offer = !canRead ? page.upsell?.offer : void 0;
8793
9437
  const tier = offer ? authority.get(offer.tierId) : void 0;
8794
9438
  const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
@@ -8801,8 +9445,11 @@ function projectMemberContent(content, viewer, tiers) {
8801
9445
  audience: page.audience,
8802
9446
  source: page.source,
8803
9447
  access: canRead ? "allowed" : "locked",
9448
+ kind: page.kind ?? "markdown",
9449
+ ...page.placeholder ? { placeholder: page.placeholder } : {},
9450
+ ...page.feature ? { feature: page.feature } : {},
8804
9451
  ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
8805
- ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
9452
+ ...canRead && page.bodyMarkdown !== void 0 ? { bodyMarkdown: page.bodyMarkdown } : {},
8806
9453
  ...upsell ? { upsell } : {}
8807
9454
  };
8808
9455
  })
@@ -8814,33 +9461,33 @@ var control = (row2) => row2 && row2.document && typeof row2.document === "objec
8814
9461
  var local2 = (row2) => row2 && row2.document && typeof row2.document === "object" ? row2 : null;
8815
9462
  async function exists(db, ns, where) {
8816
9463
  if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
8817
- const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
8818
- return rows5.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
9464
+ const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
9465
+ return rows6.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
8819
9466
  }
8820
9467
  async function loadActiveMemberContentControl(db, targetId, environment) {
8821
9468
  const where = { targetId, environment, active: true };
8822
9469
  if (!await exists(db, "memberContentControls", where)) return null;
8823
- const rows5 = (await db.query({ memberContentControls: { $: {
9470
+ const rows6 = (await db.query({ memberContentControls: { $: {
8824
9471
  where,
8825
9472
  order: { revision: "desc" },
8826
9473
  limit: 1
8827
9474
  } } })).memberContentControls;
8828
- return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
9475
+ return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
8829
9476
  }
8830
9477
  async function loadLatestMemberContentControl(db, targetId, environment) {
8831
9478
  const where = { targetId, environment };
8832
9479
  if (!await exists(db, "memberContentControls", where)) return null;
8833
- const rows5 = (await db.query({ memberContentControls: { $: {
9480
+ const rows6 = (await db.query({ memberContentControls: { $: {
8834
9481
  where,
8835
9482
  order: { revision: "desc" },
8836
9483
  limit: 1
8837
9484
  } } })).memberContentControls;
8838
- return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
9485
+ return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
8839
9486
  }
8840
9487
  async function loadLocalMemberContent(db, chapterId, environment) {
8841
9488
  const id7 = `member-content-local:${environment}`;
8842
- const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
8843
- return local2(Array.isArray(rows5) ? rows5[0] : void 0);
9489
+ const rows6 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
9490
+ return local2(Array.isArray(rows6) ? rows6[0] : void 0);
8844
9491
  }
8845
9492
  async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
8846
9493
  const { document, digest } = envelope;
@@ -8887,7 +9534,8 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
8887
9534
  var ADMIN_PATH = "/api/admin/member-content";
8888
9535
  var BODY_CAP = 1e6;
8889
9536
  var reject4 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
8890
- async function groupAndTiers(db, chapterId) {
9537
+ async function groupAndTiers(db, chapterId, mode = "chapter") {
9538
+ if (mode === "hub") return { group: null, tiers: [] };
8891
9539
  const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
8892
9540
  const group = Array.isArray(groups) ? groups[0] : void 0;
8893
9541
  if (!group) return { group: null, tiers: [] };
@@ -8924,7 +9572,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
8924
9572
  if (document.source.appId !== settings.sourceId) return reject4("wrong_source", 403);
8925
9573
  if (document.target?.chapterId !== ctx.chapter.id) return reject4("wrong_target", 409);
8926
9574
  if (document.target.environment !== env.ODLA_ENV) return reject4("wrong_environment", 409);
8927
- const { tiers } = await groupAndTiers(db, ctx.chapter.id);
9575
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode);
8928
9576
  const checked = validateMemberContentDocument(document, {
8929
9577
  sourceType: "parent",
8930
9578
  sourceAppId: settings.sourceId,
@@ -8978,17 +9626,13 @@ var handleMemberContent = async (req, url, env, ctx) => {
8978
9626
  const user = await ctx.verifyUser(req, env);
8979
9627
  if (!user) return json({ error: "unauthorized" }, 401);
8980
9628
  const db = ctx.makeDb(env);
8981
- let tierId;
8982
- try {
8983
- const member = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, { userId: user.userId, email: user.email });
8984
- if (!member || !await chapterMembershipAccess(db, env, ctx, member.source)) return json({ error: "member access required" }, 403);
8985
- tierId = typeof member.source.tierId === "string" ? member.source.tierId : null;
8986
- } catch {
8987
- return json({ error: "membership access unavailable" }, 503);
8988
- }
9629
+ const resolved = await resolvePortalMember(db, env, ctx, user);
9630
+ if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
9631
+ if (resolved.status !== "member") return json({ error: "member access required" }, 403);
9632
+ const tierId = resolved.member.tierId;
8989
9633
  const [{ parent, chapter }, { group, tiers }] = await Promise.all([
8990
9634
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
8991
- groupAndTiers(db, ctx.chapter.id)
9635
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
8992
9636
  ]);
8993
9637
  const effective = composeMemberContent(parent?.document, chapter?.document);
8994
9638
  let offerable = tiers;
@@ -9007,7 +9651,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9007
9651
  if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
9008
9652
  const [{ parent, chapter }, { tiers }] = await Promise.all([
9009
9653
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
9010
- groupAndTiers(db, ctx.chapter.id)
9654
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
9011
9655
  ]);
9012
9656
  if (req.method === "GET") {
9013
9657
  const effective = composeMemberContent(parent?.document, chapter?.document);
@@ -9015,7 +9659,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9015
9659
  parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
9016
9660
  chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
9017
9661
  effective,
9018
- tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
9662
+ tiers: tiers.map(({ id: id7, name: name2, priceCents, active }) => ({ id: id7, name: name2, priceCents, active })),
9019
9663
  localAuthoring: ctx.chapter.memberContent.localAuthoring
9020
9664
  });
9021
9665
  }
@@ -9042,6 +9686,8 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9042
9686
  parentPages: parent?.document.pages ?? []
9043
9687
  });
9044
9688
  if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
9689
+ const tiered = ctx.chapter.mode === "hub" ? checked.value.pages.findIndex((page) => page.audience.kind === "tiers") : -1;
9690
+ if (tiered >= 0) return json({ error: "invalid member content", errors: [`document.pages[${tiered}].audience must be all-members: a hub has no tiers`] }, 400);
9045
9691
  try {
9046
9692
  const saved = await saveLocalMemberContent(
9047
9693
  db,
@@ -9063,13 +9709,521 @@ function withLocalAuthority(value, revision, generatedAt, appId) {
9063
9709
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
9064
9710
  return {
9065
9711
  ...value,
9066
- schemaVersion: 1,
9712
+ // The author chooses the contract version; anything else defaults to 1.
9713
+ schemaVersion: value.schemaVersion === 2 ? 2 : 1,
9067
9714
  revision,
9068
9715
  generatedAt,
9069
9716
  source: { type: "chapter", appId }
9070
9717
  };
9071
9718
  }
9072
9719
 
9720
+ // src/portal-http.ts
9721
+ async function readJsonObject(req, maxBytes) {
9722
+ const text11 = await readBoundedRequestText(req, maxBytes);
9723
+ if (text11 instanceof Response) return text11;
9724
+ try {
9725
+ const value = JSON.parse(text11);
9726
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
9727
+ } catch {
9728
+ }
9729
+ return json({ error: "invalid JSON body" }, 400);
9730
+ }
9731
+
9732
+ // src/worker-routes-hub-members.ts
9733
+ var HUB_MEMBERS_PATH = "/api/admin/hub-members";
9734
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9735
+ async function grantId(emailNormalized) {
9736
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(emailNormalized));
9737
+ return `hub-member:${Array.from(new Uint8Array(digest).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
9738
+ }
9739
+ var view = (row2) => ({
9740
+ id: row2.id,
9741
+ email: row2.emailNormalized,
9742
+ firstName: row2.firstName ?? null,
9743
+ lastName: row2.lastName ?? null,
9744
+ status: row2.status,
9745
+ grantedBy: row2.grantedBy,
9746
+ grantedAt: row2.grantedAt,
9747
+ revokedBy: row2.revokedBy ?? null,
9748
+ revokedAt: row2.revokedAt ?? null,
9749
+ revision: row2.revision
9750
+ });
9751
+ var name = (value) => typeof value === "string" && value.trim() ? value.trim().slice(0, 80) : void 0;
9752
+ var handleAdminHubMembers = async (req, url, env, ctx) => {
9753
+ if (url.pathname !== HUB_MEMBERS_PATH && !url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) return null;
9754
+ if (ctx.chapter.mode !== "hub" || !servesMembers(ctx)) return json({ error: "not found" }, 404);
9755
+ const user = await ctx.verifyUser(req, env);
9756
+ if (!user) return json({ error: "unauthorized" }, 401);
9757
+ const db = ctx.makeDb(env);
9758
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
9759
+ const actor = user.email ?? user.userId;
9760
+ if (req.method === "GET" && url.pathname === HUB_MEMBERS_PATH) {
9761
+ const rows6 = (await db.query({ hubMemberGrants: { $: { limit: 1e3 } } })).hubMemberGrants ?? [];
9762
+ return json({ members: rows6.map(view).sort((a, b) => Number(b.grantedAt) - Number(a.grantedAt)) });
9763
+ }
9764
+ if (req.method === "POST" && url.pathname === HUB_MEMBERS_PATH) {
9765
+ const body = await readJsonObject(req, 8192);
9766
+ if (body instanceof Response) return body;
9767
+ const email = typeof body.email === "string" ? body.email.trim() : "";
9768
+ if (!EMAIL.test(email)) return json({ error: "a valid email is required" }, 400);
9769
+ const emailNormalized = normalizeChapterEmail(email);
9770
+ const id7 = await grantId(emailNormalized);
9771
+ const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
9772
+ if (current?.status === "active") return json({ member: view(current), status: "already-active" });
9773
+ const revision = Number(current?.revision ?? 0) + 1;
9774
+ const attrs = {
9775
+ id: id7,
9776
+ emailNormalized,
9777
+ status: "active",
9778
+ grantedBy: actor,
9779
+ grantedAt: Date.now(),
9780
+ revision,
9781
+ ...name(body.firstName) ? { firstName: name(body.firstName) } : {},
9782
+ ...name(body.lastName) ? { lastName: name(body.lastName) } : {}
9783
+ };
9784
+ const ops = [{ t: "update", ns: "hubMemberGrants", id: id7, attrs }];
9785
+ if (current) ops.push({ t: "retract", ns: "hubMemberGrants", id: id7, attrs: ["revokedBy", "revokedAt"] });
9786
+ try {
9787
+ await db.transact(ops, { mutationId: `hub-member-grant:${id7}:${revision}`, guards: [current ? { ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } } : { ns: "hubMemberGrants", id: id7, exists: false }] });
9788
+ } catch {
9789
+ return json({ error: "the roster changed; reload and try again" }, 409);
9790
+ }
9791
+ return json({ member: view({ ...current, ...attrs, revokedBy: void 0, revokedAt: void 0 }), status: current ? "re-granted" : "granted" }, current ? 200 : 201);
9792
+ }
9793
+ if (req.method === "DELETE" && url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) {
9794
+ const id7 = decodeURIComponent(url.pathname.slice(HUB_MEMBERS_PATH.length + 1));
9795
+ const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
9796
+ if (!current) return json({ error: "not found" }, 404);
9797
+ if (current.status !== "active") return json({ member: view(current), status: "already-revoked" });
9798
+ const attrs = { status: "revoked", revokedBy: actor, revokedAt: Date.now(), revision: Number(current.revision) + 1 };
9799
+ try {
9800
+ await db.transact([{ t: "update", ns: "hubMemberGrants", id: id7, attrs }], {
9801
+ mutationId: `hub-member-revoke:${id7}:${attrs.revision}`,
9802
+ guards: [{ ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } }]
9803
+ });
9804
+ } catch {
9805
+ return json({ error: "the roster changed; reload and try again" }, 409);
9806
+ }
9807
+ return json({ member: view({ ...current, ...attrs }), status: "revoked" });
9808
+ }
9809
+ return json({ error: "method not allowed" }, 405);
9810
+ };
9811
+
9812
+ // src/image-metadata.ts
9813
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
9814
+ var PNG_KEEP = /* @__PURE__ */ new Set(["IHDR", "PLTE", "IDAT", "IEND", "tRNS", "gAMA", "cHRM", "sRGB", "sBIT", "bKGD", "pHYs"]);
9815
+ var JPEG_KEEP = (marker) => marker === 224 || marker === 238 || marker === 219 || marker === 196 || marker === 221 || marker >= 192 && marker <= 207 && marker !== 200;
9816
+ var u16 = (b, i) => b[i] << 8 | b[i + 1];
9817
+ var u32 = (b, i) => (b[i] << 24 >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
9818
+ function concat(parts) {
9819
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
9820
+ let offset = 0;
9821
+ for (const part of parts) {
9822
+ out.set(part, offset);
9823
+ offset += part.length;
9824
+ }
9825
+ return out;
9826
+ }
9827
+ function cleanPng(bytes) {
9828
+ const parts = [bytes.subarray(0, 8)];
9829
+ let width = 0, height = 0, offset = 8, ended = false;
9830
+ while (offset + 12 <= bytes.length) {
9831
+ const length = u32(bytes, offset);
9832
+ const end = offset + 12 + length;
9833
+ if (end > bytes.length) return null;
9834
+ const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
9835
+ if (type === "IHDR") {
9836
+ width = u32(bytes, offset + 8);
9837
+ height = u32(bytes, offset + 12);
9838
+ }
9839
+ if (PNG_KEEP.has(type)) parts.push(bytes.subarray(offset, end));
9840
+ offset = end;
9841
+ if (type === "IEND") {
9842
+ ended = true;
9843
+ break;
9844
+ }
9845
+ }
9846
+ if (!ended || !width || !height) return null;
9847
+ return { contentType: "image/png", extension: "png", width, height, bytes: concat(parts) };
9848
+ }
9849
+ function cleanJpeg(bytes) {
9850
+ const parts = [bytes.subarray(0, 2)];
9851
+ let width = 0, height = 0, offset = 2;
9852
+ while (offset + 4 <= bytes.length) {
9853
+ if (bytes[offset] !== 255) return null;
9854
+ const marker = bytes[offset + 1];
9855
+ if (marker === 255) {
9856
+ offset += 1;
9857
+ continue;
9858
+ }
9859
+ const length = u16(bytes, offset + 2);
9860
+ const end = offset + 2 + length;
9861
+ if (length < 2 || end > bytes.length) return null;
9862
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
9863
+ height = u16(bytes, offset + 5);
9864
+ width = u16(bytes, offset + 7);
9865
+ }
9866
+ if (marker === 218) {
9867
+ parts.push(bytes.subarray(offset));
9868
+ return width && height ? { contentType: "image/jpeg", extension: "jpg", width, height, bytes: concat(parts) } : null;
9869
+ }
9870
+ if (JPEG_KEEP(marker)) parts.push(bytes.subarray(offset, end));
9871
+ offset = end;
9872
+ }
9873
+ return null;
9874
+ }
9875
+ function cleanImage(bytes) {
9876
+ if (bytes.length > 8 && PNG_SIGNATURE.every((byte, index2) => bytes[index2] === byte)) return cleanPng(bytes);
9877
+ if (bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return cleanJpeg(bytes);
9878
+ return null;
9879
+ }
9880
+
9881
+ // src/member-profile.ts
9882
+ var MEMBER_PROFILE_FIELDS = {
9883
+ preferredName: "person.preferredName",
9884
+ location: "person.region",
9885
+ prompts: "person.profilePrompts",
9886
+ introducedByText: "person.introducedByText"
9887
+ };
9888
+ var MAX_PROMPT_ANSWER = 280;
9889
+ var clean = (value) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
9890
+ function optionalText(body, key3, max, errors) {
9891
+ if (!(key3 in body)) return void 0;
9892
+ const value = body[key3];
9893
+ if (value === null || value === "") return null;
9894
+ if (typeof value !== "string") {
9895
+ errors.push(`${key3} must be a string`);
9896
+ return void 0;
9897
+ }
9898
+ const text11 = clean(value);
9899
+ if (text11.length > max) errors.push(`${key3} must be at most ${max} characters`);
9900
+ return text11 || null;
9901
+ }
9902
+ function validateProfilePatch(body, portal) {
9903
+ const errors = [];
9904
+ for (const key3 of Object.keys(body)) {
9905
+ if (key3 === "expectedRevision") continue;
9906
+ if (!(key3 in MEMBER_PROFILE_FIELDS)) errors.push(`${key3} is not an editable profile field`);
9907
+ else if (!MEMBER_SELF_EDITABLE_FIELDS.has(MEMBER_PROFILE_FIELDS[key3])) errors.push(`${key3} is read-only`);
9908
+ }
9909
+ const patch = {};
9910
+ const preferredName = optionalText(body, "preferredName", 80, errors);
9911
+ if (preferredName !== void 0) patch.preferredName = preferredName;
9912
+ const location = optionalText(body, "location", 80, errors);
9913
+ if (location !== void 0) patch.location = location;
9914
+ const introducedByText = optionalText(body, "introducedByText", 120, errors);
9915
+ if (introducedByText !== void 0) patch.introducedByText = introducedByText;
9916
+ if ("prompts" in body) {
9917
+ const raw2 = body.prompts;
9918
+ if (!Array.isArray(raw2)) errors.push("prompts must be an array");
9919
+ else {
9920
+ if (raw2.length > portal.maxPrompts) errors.push(`prompts supports at most ${portal.maxPrompts} answers`);
9921
+ const seen = /* @__PURE__ */ new Set();
9922
+ const prompts = [];
9923
+ for (const [index2, item] of raw2.entries()) {
9924
+ const entry = item && typeof item === "object" ? item : {};
9925
+ const prompt = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
9926
+ const answer = typeof entry.answer === "string" ? clean(entry.answer) : "";
9927
+ if (!portal.profilePrompts.includes(prompt)) errors.push(`prompts[${index2}].prompt is not one of this site's prompts`);
9928
+ if (seen.has(prompt)) errors.push(`prompts[${index2}].prompt is already answered`);
9929
+ if (entry.answer !== void 0 && typeof entry.answer !== "string") errors.push(`prompts[${index2}].answer must be a string`);
9930
+ if (answer.length > MAX_PROMPT_ANSWER) errors.push(`prompts[${index2}].answer must be at most ${MAX_PROMPT_ANSWER} characters`);
9931
+ seen.add(prompt);
9932
+ prompts.push({ prompt, answer });
9933
+ }
9934
+ patch.prompts = prompts;
9935
+ }
9936
+ }
9937
+ return errors.length ? { ok: false, errors } : { ok: true, patch };
9938
+ }
9939
+ var nameKey = (value) => value.toLowerCase().normalize("NFKC").replace(/[^\p{L}\p{N}\s'-]/gu, " ").replace(/\s+/g, " ").trim();
9940
+
9941
+ // src/member-profile-store.ts
9942
+ async function loadMemberProfile(db, memberId) {
9943
+ return (await db.query({ memberProfiles: { $: { where: { id: memberId }, limit: 1 } } })).memberProfiles?.[0] ?? null;
9944
+ }
9945
+ async function membersNamed(db, mode, chapterId, text11, selfId) {
9946
+ const wanted = nameKey(text11);
9947
+ if (!wanted) return [];
9948
+ const rows6 = mode === "hub" ? (await db.query({ hubMemberGrants: { $: { where: { status: "active" }, limit: 5e3 } } })).hubMemberGrants ?? [] : ((await db.query({ applications: { $: { where: { groupId: chapterId, status: "approved" }, limit: 5e3 } } })).applications ?? []).filter((row2) => row2.canceled !== true);
9949
+ return rows6.filter((row2) => String(row2.id) !== selfId && nameKey(`${row2.firstName ?? ""} ${row2.lastName ?? ""}`) === wanted).map((row2) => String(row2.id));
9950
+ }
9951
+ function reviewItemOp(id7, kind, detail, memberId, relatedMemberId) {
9952
+ return { t: "update", ns: "portalReviewItems", id: id7, attrs: {
9953
+ id: id7,
9954
+ kind,
9955
+ detail,
9956
+ status: "open",
9957
+ createdAt: Date.now(),
9958
+ ...memberId ? { memberId } : {},
9959
+ ...relatedMemberId ? { relatedMemberId } : {}
9960
+ } };
9961
+ }
9962
+ async function saveMemberProfile(db, input) {
9963
+ const current = await loadMemberProfile(db, input.memberId);
9964
+ if (Number(current?.revision ?? 0) !== input.expectedRevision) throw new Error("member_profile_revision_conflict");
9965
+ const now = Date.now();
9966
+ const revision = input.expectedRevision + 1;
9967
+ const attrs = { id: input.memberId, memberId: input.memberId, revision, updatedAt: now };
9968
+ const retract = [];
9969
+ const set = (key3, value) => {
9970
+ if (value === null) retract.push(key3);
9971
+ else attrs[key3] = value;
9972
+ };
9973
+ if (input.patch.preferredName !== void 0) set("preferredName", input.patch.preferredName);
9974
+ if (input.patch.location !== void 0) set("location", input.patch.location);
9975
+ if (input.patch.prompts !== void 0) attrs.prompts = input.patch.prompts;
9976
+ const ops = [];
9977
+ let review = null;
9978
+ const introduced = input.patch.introducedByText;
9979
+ if (introduced !== void 0 && introduced !== (current?.introducedByText ?? null)) {
9980
+ set("introducedByText", introduced);
9981
+ if (introduced === null) retract.push("introducedByMemberId", "introducedAt");
9982
+ else {
9983
+ const matches2 = await membersNamed(db, input.mode, input.chapterId, introduced, input.memberId);
9984
+ if (matches2.length === 1) {
9985
+ Object.assign(attrs, { introducedByMemberId: matches2[0], introducedAt: now });
9986
+ review = "matched";
9987
+ } else {
9988
+ retract.push("introducedByMemberId", "introducedAt");
9989
+ ops.push(reviewItemOp(
9990
+ `introduction:${input.memberId}:${revision}`,
9991
+ "introduction_unmatched",
9992
+ { text: introduced, candidates: matches2.length },
9993
+ input.memberId
9994
+ ));
9995
+ review = "queued";
9996
+ }
9997
+ }
9998
+ }
9999
+ ops.unshift({ t: "update", ns: "memberProfiles", id: input.memberId, attrs });
10000
+ const absent = retract.filter((key3) => current && current[key3] !== void 0);
10001
+ if (absent.length) ops.splice(1, 0, { t: "retract", ns: "memberProfiles", id: input.memberId, attrs: absent });
10002
+ await db.transact(ops, {
10003
+ mutationId: `member-profile:${input.memberId}:${revision}`,
10004
+ guards: [current ? { ns: "memberProfiles", id: input.memberId, exists: true, equals: { revision: input.expectedRevision } } : { ns: "memberProfiles", id: input.memberId, exists: false }]
10005
+ });
10006
+ const row2 = { ...current, ...attrs };
10007
+ for (const key3 of absent) delete row2[key3];
10008
+ return { row: row2, review };
10009
+ }
10010
+
10011
+ // src/worker-routes-member-profile.ts
10012
+ var MEMBER_PROFILE_PATH = "/api/member/profile";
10013
+ var MEMBER_PHOTO_PATH = "/api/member/profile/photo";
10014
+ var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
10015
+ var PHOTO_SIZE = 400;
10016
+ async function portalMemberGate(req, env, ctx) {
10017
+ const user = await ctx.verifyUser(req, env);
10018
+ if (!user) return json({ error: "unauthorized" }, 401);
10019
+ const db = ctx.makeDb(env);
10020
+ const resolved = await resolvePortalMember(db, env, ctx, user);
10021
+ if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
10022
+ if (resolved.status !== "member") return json({ error: "member access required" }, 403);
10023
+ return { db, member: resolved.member };
10024
+ }
10025
+ async function profileView(db, ctx, member) {
10026
+ const row2 = await loadMemberProfile(db, member.memberId);
10027
+ const portal = ctx.chapter.memberPortal;
10028
+ return {
10029
+ revision: Number(row2?.revision ?? 0),
10030
+ identity: await portalIdentity(db, ctx, member),
10031
+ profile: {
10032
+ preferredName: row2?.preferredName ?? null,
10033
+ location: row2?.location ?? null,
10034
+ prompts: Array.isArray(row2?.prompts) ? row2.prompts : [],
10035
+ introducedByText: row2?.introducedByText ?? null,
10036
+ introducedByMatched: typeof row2?.introducedByMemberId === "string"
10037
+ },
10038
+ promptChoices: portal.profilePrompts,
10039
+ maxPrompts: portal.maxPrompts,
10040
+ photos: portal.photos
10041
+ };
10042
+ }
10043
+ var handleMemberProfile = async (req, url, env, ctx) => {
10044
+ if (url.pathname !== MEMBER_PROFILE_PATH && url.pathname !== MEMBER_PHOTO_PATH) return null;
10045
+ const portal = ctx.chapter.memberPortal;
10046
+ if (!portal) return json({ error: "not found" }, 404);
10047
+ const gate12 = await portalMemberGate(req, env, ctx);
10048
+ if (gate12 instanceof Response) return gate12;
10049
+ const { db, member } = gate12;
10050
+ if (url.pathname === MEMBER_PHOTO_PATH) return uploadPhoto(req, db, ctx, member);
10051
+ if (req.method === "GET") return json(await profileView(db, ctx, member));
10052
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
10053
+ const body = await readJsonObject(req, 16384);
10054
+ if (body instanceof Response) return body;
10055
+ const expectedRevision = body.expectedRevision;
10056
+ if (!Number.isInteger(expectedRevision) || Number(expectedRevision) < 0) {
10057
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
10058
+ }
10059
+ const checked = validateProfilePatch(body, portal);
10060
+ if (!checked.ok) return json({ error: "invalid profile", errors: checked.errors }, 400);
10061
+ try {
10062
+ const saved = await saveMemberProfile(db, {
10063
+ mode: ctx.chapter.mode,
10064
+ chapterId: ctx.chapter.id,
10065
+ memberId: member.memberId,
10066
+ patch: checked.patch,
10067
+ expectedRevision: Number(expectedRevision)
10068
+ });
10069
+ return json({ ...await profileView(db, ctx, member), introducedBy: saved.review });
10070
+ } catch (error) {
10071
+ if (error instanceof Error && error.message === "member_profile_revision_conflict") {
10072
+ return json({ error: "your profile changed elsewhere; reload before saving" }, 409);
10073
+ }
10074
+ return json({ error: "profile could not be saved" }, 503);
10075
+ }
10076
+ };
10077
+ async function uploadPhoto(req, db, ctx, member) {
10078
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
10079
+ if (!ctx.chapter.memberPortal?.photos || !db.storage) return json({ error: "photos are not enabled" }, 404);
10080
+ const declared = Number(req.headers.get("content-length") ?? 0);
10081
+ if (declared > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
10082
+ const bytes = new Uint8Array(await req.arrayBuffer());
10083
+ if (bytes.length > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
10084
+ const image = cleanImage(bytes);
10085
+ if (!image) return json({ error: "photo must be a JPEG or PNG" }, 415);
10086
+ if (image.width > PHOTO_SIZE || image.height > PHOTO_SIZE) {
10087
+ return json({ error: `photo must be at most ${PHOTO_SIZE} by ${PHOTO_SIZE} pixels` }, 422);
10088
+ }
10089
+ const now = Date.now();
10090
+ const path = `member-photos/${encodeURIComponent(member.memberId)}/${now}.${image.extension}`;
10091
+ await db.storage.upload(path, new Blob([image.bytes], { type: image.contentType }), image.contentType, { private: true });
10092
+ const previous = await loadMemberProfile(db, member.memberId);
10093
+ await db.transact([{ t: "update", ns: "memberProfiles", id: member.memberId, attrs: {
10094
+ id: member.memberId,
10095
+ memberId: member.memberId,
10096
+ photoPath: path,
10097
+ photoUpdatedAt: now,
10098
+ updatedAt: now,
10099
+ ...previous ? {} : { revision: 0 }
10100
+ } }], { mutationId: `member-photo:${member.memberId}:${now}` });
10101
+ const old = typeof previous?.photoPath === "string" ? previous.photoPath : null;
10102
+ if (old && old !== path) await db.storage.delete(old).catch(() => void 0);
10103
+ return json({ photoUrl: await db.storage.sign(path, 3600), width: image.width, height: image.height });
10104
+ }
10105
+
10106
+ // src/worker-routes-admin-member-tier.ts
10107
+ var PATH5 = /^\/api\/admin\/applications\/([^/]+)\/entitlement-tier$/;
10108
+ var handleAdminMemberTier = async (req, url, env, ctx) => {
10109
+ const match = PATH5.exec(url.pathname);
10110
+ if (!match) return null;
10111
+ if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
10112
+ if (!["GET", "POST", "DELETE"].includes(req.method)) return json({ error: "method not allowed" }, 405);
10113
+ const user = await ctx.verifyUser(req, env);
10114
+ if (!user) return json({ error: "unauthorized" }, 401);
10115
+ const db = ctx.makeDb(env);
10116
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
10117
+ const id7 = decodeURIComponent(match[1]);
10118
+ const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
10119
+ if (!app || app.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
10120
+ const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
10121
+ const tiers = group ? await loadTiers(db, group) : [];
10122
+ if (req.method === "GET") {
10123
+ const history = (await db.query({ memberTierChanges: { $: { where: { applicationId: id7 }, limit: 50 } } })).memberTierChanges ?? [];
10124
+ return json({
10125
+ purchasedTierId: app.tierId ?? null,
10126
+ entitledTierId: entitledTierId(app),
10127
+ entitlementTierEndsAt: app.entitlementTierEndsAt ?? null,
10128
+ tiers: tiers.filter((tier) => tier.active !== false).map(({ id: tierId, name: name2 }) => ({ id: tierId, name: name2 })),
10129
+ history: history.sort((a, b) => Number(b.at) - Number(a.at))
10130
+ });
10131
+ }
10132
+ const body = await readJsonObject(req, 8192);
10133
+ if (body instanceof Response) return body;
10134
+ if (app.status !== "approved" || app.canceled === true) return json({ error: "only an approved member can change tier" }, 409);
10135
+ const reason = typeof body.reason === "string" ? body.reason.trim() : "";
10136
+ if (!reason || reason.length > 500) return json({ error: "a reason of up to 500 characters is required" }, 400);
10137
+ const paymentReference = typeof body.paymentReference === "string" && body.paymentReference.trim() ? body.paymentReference.trim().slice(0, 200) : void 0;
10138
+ const now = Date.now();
10139
+ let toTierId;
10140
+ let endsAt;
10141
+ if (req.method === "POST") {
10142
+ toTierId = typeof body.tierId === "string" ? body.tierId : "";
10143
+ const tier = tiers.find((candidate) => candidate.id === toTierId);
10144
+ if (!tier || tier.active === false) return json({ error: "tierId must be an active tier" }, 400);
10145
+ if (body.endsAt !== void 0) {
10146
+ endsAt = Number(body.endsAt);
10147
+ if (!Number.isSafeInteger(endsAt) || endsAt <= now) return json({ error: "endsAt must be a future timestamp" }, 400);
10148
+ }
10149
+ } else if (app.entitlementTierId === void 0) return json({ error: "no entitlement tier to revert" }, 409);
10150
+ const fromTierId = entitledTierId(app, now) ?? void 0;
10151
+ const changeId = `tier-change:${id7}:${now}`;
10152
+ const ops = [
10153
+ { t: "update", ns: "memberTierChanges", id: changeId, attrs: {
10154
+ id: changeId,
10155
+ applicationId: id7,
10156
+ reason,
10157
+ actor: user.email ?? user.userId,
10158
+ at: now,
10159
+ ...fromTierId ? { fromTierId } : {},
10160
+ ...toTierId ? { toTierId } : {},
10161
+ ...endsAt ? { endsAt } : {},
10162
+ ...paymentReference ? { paymentReference } : {}
10163
+ } },
10164
+ toTierId ? { t: "update", ns: "applications", id: id7, attrs: { entitlementTierId: toTierId, ...endsAt ? { entitlementTierEndsAt: endsAt } : {} } } : { t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierId", "entitlementTierEndsAt"] }
10165
+ ];
10166
+ if (toTierId && !endsAt && app.entitlementTierEndsAt !== void 0) ops.push({ t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierEndsAt"] });
10167
+ const current = app.entitlementTierId;
10168
+ try {
10169
+ await db.transact(ops, { mutationId: changeId, guards: [current === void 0 ? { ns: "applications", id: id7, exists: true, equals: { status: "approved" }, absent: ["entitlementTierId"] } : { ns: "applications", id: id7, exists: true, equals: { status: "approved", entitlementTierId: current } }] });
10170
+ } catch {
10171
+ return json({ error: "this member changed; reload and try again" }, 409);
10172
+ }
10173
+ return json({ ok: true, applicationId: id7, fromTierId: fromTierId ?? null, toTierId: toTierId ?? (typeof app.tierId === "string" ? app.tierId : null), endsAt: endsAt ?? null });
10174
+ };
10175
+
10176
+ // src/worker-routes-admin-portal-review.ts
10177
+ var PORTAL_REVIEW_PATH = "/api/admin/portal-review";
10178
+ var handleAdminPortalReview = async (req, url, env, ctx) => {
10179
+ if (url.pathname !== PORTAL_REVIEW_PATH && !url.pathname.startsWith(`${PORTAL_REVIEW_PATH}/`)) return null;
10180
+ if (!ctx.chapter.memberPortal) return json({ error: "not found" }, 404);
10181
+ const user = await ctx.verifyUser(req, env);
10182
+ if (!user) return json({ error: "unauthorized" }, 401);
10183
+ const db = ctx.makeDb(env);
10184
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
10185
+ if (req.method === "GET" && url.pathname === PORTAL_REVIEW_PATH) {
10186
+ const status2 = url.searchParams.get("status") ?? "open";
10187
+ const where = status2 === "all" ? {} : { status: status2 };
10188
+ const items = (await db.query({ portalReviewItems: { $: { where, limit: 1e3 } } })).portalReviewItems ?? [];
10189
+ return json({ items: items.sort((a, b) => Number(b.createdAt) - Number(a.createdAt)) });
10190
+ }
10191
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
10192
+ const id7 = decodeURIComponent(url.pathname.slice(PORTAL_REVIEW_PATH.length + 1));
10193
+ const body = await readJsonObject(req, 4096);
10194
+ if (body instanceof Response) return body;
10195
+ const decision = typeof body.decision === "string" ? body.decision.trim() : "";
10196
+ if (!decision || decision.length > 1e3) return json({ error: "a decision of up to 1,000 characters is required" }, 400);
10197
+ const item = (await db.query({ portalReviewItems: { $: { where: { id: id7 }, limit: 1 } } })).portalReviewItems?.[0];
10198
+ if (!item) return json({ error: "not found" }, 404);
10199
+ if (item.status !== "open") return json({ error: "already resolved" }, 409);
10200
+ const attrs = { status: "resolved", decision, resolvedBy: user.email ?? user.userId, resolvedAt: Date.now() };
10201
+ try {
10202
+ await db.transact([{ t: "update", ns: "portalReviewItems", id: id7, attrs }], {
10203
+ mutationId: `portal-review:${id7}:resolve`,
10204
+ guards: [{ ns: "portalReviewItems", id: id7, exists: true, equals: { status: "open" } }]
10205
+ });
10206
+ } catch {
10207
+ return json({ error: "already resolved" }, 409);
10208
+ }
10209
+ return json({ item: { ...item, ...attrs } });
10210
+ };
10211
+
10212
+ // src/worker-routes-member-portal.ts
10213
+ var PORTAL_ROUTES = [
10214
+ handleMemberProfile,
10215
+ handleAdminHubMembers,
10216
+ handleAdminMemberTier,
10217
+ handleAdminPortalReview
10218
+ ];
10219
+ var handleMemberPortal = async (req, url, env, ctx) => {
10220
+ for (const route of PORTAL_ROUTES) {
10221
+ const response = await route(req, url, env, ctx);
10222
+ if (response) return response;
10223
+ }
10224
+ return null;
10225
+ };
10226
+
9073
10227
  // src/worker-routes-admission-grants.ts
9074
10228
  var import_db5 = require("@odla-ai/db");
9075
10229
  var reject5 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
@@ -9233,7 +10387,7 @@ async function networkWriteAllowed(req, db, user, ctx, sensitiveRead = false) {
9233
10387
 
9234
10388
  // src/network-person-reference.ts
9235
10389
  var import_db6 = require("@odla-ai/db");
9236
- var import_crm11 = require("@odla-ai/crm");
10390
+ var import_crm12 = require("@odla-ai/crm");
9237
10391
 
9238
10392
  // src/worker-network-fetch.ts
9239
10393
  var NetworkTargetTransportError = class extends Error {
@@ -9269,7 +10423,7 @@ function fetchNetworkTarget(env, target, destination, init) {
9269
10423
  }
9270
10424
 
9271
10425
  // src/worker-network-admission-state.ts
9272
- var import_crm10 = require("@odla-ai/crm");
10426
+ var import_crm11 = require("@odla-ai/crm");
9273
10427
  var MAX_ASSIGNMENTS = 2e3;
9274
10428
  var admissionRows = (value) => Array.isArray(value) ? value : [];
9275
10429
  function assignmentId(targetId, personId) {
@@ -9296,7 +10450,7 @@ function personAdmissionEmail(ctx, record4) {
9296
10450
  return isValidEmail(normalized) ? normalized : null;
9297
10451
  }
9298
10452
  async function grantInput(db, ctx, assignment, resolveEmail) {
9299
- const person = resolveEmail ? null : await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
10453
+ const person = resolveEmail ? null : await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
9300
10454
  const email = resolveEmail ? await resolveEmail(assignment.personId) : person ? personAdmissionEmail(ctx, person) : null;
9301
10455
  if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
9302
10456
  return {
@@ -9450,9 +10604,9 @@ function integer3(value, fallback, min, max) {
9450
10604
  const parsed = Number(value);
9451
10605
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
9452
10606
  }
9453
- function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
10607
+ function projectRecord(record4, allowed, editable, stagesEnabled, crm) {
9454
10608
  const fields = Object.fromEntries(
9455
- allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
10609
+ allowed.flatMap((name2) => Object.prototype.hasOwnProperty.call(record4.fields, name2) ? [[name2, record4.fields[name2]]] : [])
9456
10610
  );
9457
10611
  const def = crm.type(record4.type);
9458
10612
  const editFields = editable.map((id7) => {
@@ -9502,7 +10656,7 @@ function chapterPersonReference(targetId, recordId) {
9502
10656
  }
9503
10657
  async function resolveAdmissionPersonEmail(db, env, ctx, target, personId) {
9504
10658
  if (!personId.startsWith("chapter-person:")) {
9505
- const person2 = await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, personId);
10659
+ const person2 = await (0, import_crm12.getRecord)({ crm: ctx.chapter.crm, db }, personId);
9506
10660
  return person2 ? personAdmissionEmail(ctx, person2) : null;
9507
10661
  }
9508
10662
  const prefix = `chapter-person:${target.id}:`;
@@ -9562,7 +10716,7 @@ async function deliverAdmissionGrants(options) {
9562
10716
  }
9563
10717
 
9564
10718
  // src/worker-routes-network-admission.ts
9565
- var PATH5 = "/api/admin/network/admissions";
10719
+ var PATH6 = "/api/admin/network/admissions";
9566
10720
  var RETRY_PATH = "/api/admin/network/admissions/retry";
9567
10721
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
9568
10722
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -9642,11 +10796,11 @@ async function parseBody(req) {
9642
10796
  }
9643
10797
  }
9644
10798
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
9645
- if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
10799
+ if (![PATH6, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
9646
10800
  const auth = await gate2(req, env, ctx);
9647
10801
  if ("response" in auth) return auth.response;
9648
10802
  const db = auth.db;
9649
- if (url.pathname === PATH5 && req.method === "GET") {
10803
+ if (url.pathname === PATH6 && req.method === "GET") {
9650
10804
  const targetId2 = url.searchParams.get("targetId") ?? "";
9651
10805
  const target2 = targetFor(ctx, targetId2);
9652
10806
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -9794,8 +10948,8 @@ function makeCalendar(env) {
9794
10948
  }
9795
10949
  async function firstRow3(db, ns, q) {
9796
10950
  const res = await db.query({ [ns]: { $: q } });
9797
- const rows5 = res[ns];
9798
- return Array.isArray(rows5) ? rows5[0] : void 0;
10951
+ const rows6 = res[ns];
10952
+ return Array.isArray(rows6) ? rows6[0] : void 0;
9799
10953
  }
9800
10954
  async function computeSlots(cal, cfg) {
9801
10955
  const { from, to } = slotWindow(Date.now(), cfg.windowDays);
@@ -10654,18 +11808,18 @@ async function startSubscription(req, env, ctx) {
10654
11808
 
10655
11809
  // src/worker-payment-receipt.ts
10656
11810
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10657
- var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
11811
+ var escape = (text11) => text11.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10658
11812
  async function paymentReceiptRoute(req, url, env, ctx) {
10659
11813
  const id7 = url.searchParams.get("application");
10660
11814
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
10661
11815
  if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
10662
11816
  const db = ctx.makeDb(env);
10663
- const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
10664
- const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
11817
+ const rows6 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
11818
+ const app = rows6.applications?.[0], intent = rows6.subscriptionCheckoutIntents?.[0];
10665
11819
  if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
10666
11820
  const receipt = intent.paymentReceipt;
10667
11821
  if (!receipt) return json2({ pending: true }, 202);
10668
- const text10 = paymentReceiptText(receipt, ctx.chapter.name);
11822
+ const text11 = paymentReceiptText(receipt, ctx.chapter.name);
10669
11823
  if (req.method === "POST") {
10670
11824
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10671
11825
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10683,7 +11837,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10683
11837
  template: "paymentReceipt",
10684
11838
  to: String(intent.purchaserEmailNormalized),
10685
11839
  applicationId: id7,
10686
- vars: { chapterName: ctx.chapter.name, receiptText: text10 },
11840
+ vars: { chapterName: ctx.chapter.name, receiptText: text11 },
10687
11841
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10688
11842
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10689
11843
  });
@@ -10693,7 +11847,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10693
11847
  const nonce = crypto.randomUUID();
10694
11848
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10695
11849
  <style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
10696
- <h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
11850
+ <h1>Payment receipt</h1><pre>${escape(text11)}</pre><button id="print">Print / save PDF</button>
10697
11851
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10698
11852
  headers: {
10699
11853
  "content-type": "text/html; charset=utf-8",
@@ -10726,7 +11880,7 @@ var handlePayments = async (req, url, env, ctx) => {
10726
11880
 
10727
11881
  // src/worker-routes-gifts.ts
10728
11882
  var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
10729
- var rows3 = (value) => Array.isArray(value) ? value : [];
11883
+ var rows4 = (value) => Array.isArray(value) ? value : [];
10730
11884
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
10731
11885
  var userFor = async (req, env, ctx) => {
10732
11886
  const user = await ctx.verifyUser(req, env);
@@ -10764,7 +11918,7 @@ var handleGifts = async (req, url, env, ctx) => {
10764
11918
  const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
10765
11919
  return json({
10766
11920
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
10767
- claims: buyerGiftClaims(rows3(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
11921
+ claims: buyerGiftClaims(rows4(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
10768
11922
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
10769
11923
  merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
10770
11924
  });
@@ -10787,7 +11941,7 @@ var handleGifts = async (req, url, env, ctx) => {
10787
11941
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
10788
11942
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
10789
11943
  const emailNormalized = normalizeChapterEmail(user.email);
10790
- const applications = rows3((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
11944
+ const applications = rows4((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
10791
11945
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
10792
11946
  try {
10793
11947
  return json(await startGiftIntent(db, {
@@ -10917,9 +12071,9 @@ var receiptFor2 = (seat, released) => ({
10917
12071
  });
10918
12072
  async function regiftNamedSeat(db, input, now) {
10919
12073
  membershipClock(now);
10920
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
12074
+ const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
10921
12075
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
10922
- if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
12076
+ if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
10923
12077
  const state = await loadMembershipAuthorityState(db, input);
10924
12078
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
10925
12079
  requireMembershipScope(seat, input);
@@ -10928,7 +12082,7 @@ async function regiftNamedSeat(db, input, now) {
10928
12082
  if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
10929
12083
  if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
10930
12084
  const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
10931
- if (seat.recipientEmailNormalized === email && seat.recipientName === name && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
12085
+ if (seat.recipientEmailNormalized === email && seat.recipientName === name2 && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
10932
12086
  return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
10933
12087
  }
10934
12088
  if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
@@ -10966,7 +12120,7 @@ async function regiftNamedSeat(db, input, now) {
10966
12120
  });
10967
12121
  const ops = [
10968
12122
  membershipUpdate("networkSeat", String(seat.id), {
10969
- recipientName: name,
12123
+ recipientName: name2,
10970
12124
  recipientEmailNormalized: email,
10971
12125
  recipientClaimId: claimId,
10972
12126
  status: "pending_acceptance",
@@ -11163,16 +12317,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
11163
12317
  };
11164
12318
 
11165
12319
  // src/named-seat-accepted-notice.ts
11166
- var text7 = (value) => typeof value === "string" ? value : "";
12320
+ var text8 = (value) => typeof value === "string" ? value : "";
11167
12321
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
11168
12322
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
11169
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
12323
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text8(purchaser.email)) {
11170
12324
  return { sent: false, reason: "purchaser-missing" };
11171
12325
  }
11172
12326
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
11173
12327
  if (!group) return { sent: false, reason: "group-missing" };
11174
12328
  const config = emailGroupFrom(group);
11175
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
12329
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text8).join(" ").replace(/\s+/g, " ").trim();
11176
12330
  return sendTemplated({
11177
12331
  db,
11178
12332
  envName: env.ODLA_ENV,
@@ -11184,12 +12338,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
11184
12338
  }, {
11185
12339
  group: config,
11186
12340
  template: "seatAccepted",
11187
- to: text7(purchaser.email),
12341
+ to: text8(purchaser.email),
11188
12342
  dedupeKey: `named-seat-accepted:${input.seatId}`,
11189
12343
  applicationId: input.primaryApplicationId,
11190
12344
  tierId: input.skuId,
11191
12345
  vars: {
11192
- firstName: text7(purchaser.firstName),
12346
+ firstName: text8(purchaser.firstName),
11193
12347
  purchaserName,
11194
12348
  recipientName: input.recipientName,
11195
12349
  tierName: await tierName(db, group, input.skuId),
@@ -11401,10 +12555,10 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
11401
12555
  };
11402
12556
 
11403
12557
  // src/worker-routes-people-profile.ts
11404
- var import_crm13 = require("@odla-ai/crm");
12558
+ var import_crm14 = require("@odla-ai/crm");
11405
12559
 
11406
12560
  // src/people-profile.ts
11407
- var import_crm12 = require("@odla-ai/crm");
12561
+ var import_crm13 = require("@odla-ai/crm");
11408
12562
  var sameValue = (a, b) => (a == null || a === "") && (b == null || b === "") || JSON.stringify(a) === JSON.stringify(b);
11409
12563
  function profileReadOnlyFields(record4, ctx) {
11410
12564
  return [
@@ -11419,17 +12573,17 @@ async function updateChapterPerson(opts) {
11419
12573
  const { db, env, ctx, record: record4 } = opts;
11420
12574
  const input = Object.fromEntries(Object.entries(opts.input).filter(([key3, value]) => !sameValue(value, record4.fields[key3])));
11421
12575
  const forbidden = profileReadOnlyFields(record4, ctx).filter((key3) => key3 in input);
11422
- if (forbidden.length) throw new import_crm12.CrmInputError(
12576
+ if (forbidden.length) throw new import_crm13.CrmInputError(
11423
12577
  "Use the verified identity or application workflow for protected fields",
11424
12578
  Object.fromEntries(forbidden.map((key3) => [key3, "This field is managed by its authoritative workflow"]))
11425
12579
  );
11426
12580
  const appId = record4.fields.applicationId;
11427
12581
  const app = typeof appId === "string" && appId ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : null;
11428
12582
  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");
12583
+ throw new import_crm13.CrmInputError("The application identity must be repaired before editing this profile");
11430
12584
  }
11431
12585
  const prepared = ctx.chapter.crm.prepare(record4.type, input, record4);
11432
- if (!prepared.ok) throw new import_crm12.CrmInputError("invalid input", prepared.errors);
12586
+ if (!prepared.ok) throw new import_crm13.CrmInputError("invalid input", prepared.errors);
11433
12587
  const appAttrs = {};
11434
12588
  if (app) {
11435
12589
  const mirrored = /* @__PURE__ */ new Set(["firstName", "lastName", "phone", "linkedin", ...ctx.chapter.application.crmFields]);
@@ -11458,7 +12612,7 @@ async function updateChapterPerson(opts) {
11458
12612
  }] });
11459
12613
  }
11460
12614
  };
11461
- return (0, import_crm12.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
12615
+ return (0, import_crm13.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
11462
12616
  id: record4.id,
11463
12617
  input,
11464
12618
  expectedUpdatedAt: opts.expectedRevision ?? record4.updatedAt,
@@ -11503,7 +12657,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
11503
12657
  } catch {
11504
12658
  return json({ error: "invalid record id" }, 422);
11505
12659
  }
11506
- const record4 = await (0, import_crm13.getRecord)({ crm: ctx.chapter.crm, db }, id7);
12660
+ const record4 = await (0, import_crm14.getRecord)({ crm: ctx.chapter.crm, db }, id7);
11507
12661
  if (!record4) return json({ error: "not found" }, 404);
11508
12662
  if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
11509
12663
  if (match?.[2]) {
@@ -11555,7 +12709,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
11555
12709
  });
11556
12710
  return json(result);
11557
12711
  } catch (error) {
11558
- if (error instanceof import_crm13.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
12712
+ if (error instanceof import_crm14.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
11559
12713
  if (error.code === "transact_guard_failed") return json({ error: "profile changed; reload before saving" }, 409);
11560
12714
  return json({ error: "profile update failed" }, 503);
11561
12715
  }
@@ -11650,8 +12804,8 @@ async function upcomingEvents(env) {
11650
12804
  const res = await cal.availability.upcoming();
11651
12805
  return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
11652
12806
  }
11653
- function toReconcile(rows5) {
11654
- return rows5.map((m) => ({
12807
+ function toReconcile(rows6) {
12808
+ return rows6.map((m) => ({
11655
12809
  id: String(m.id),
11656
12810
  applicationId: String(m.applicationId),
11657
12811
  googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
@@ -11673,12 +12827,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11673
12827
  const query = await db.query({
11674
12828
  meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
11675
12829
  });
11676
- let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
11677
- if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
11678
- if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
12830
+ let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
12831
+ if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
12832
+ if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
11679
12833
  let decisions = [];
11680
12834
  try {
11681
- decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
12835
+ decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
11682
12836
  } catch {
11683
12837
  }
11684
12838
  const patched = /* @__PURE__ */ new Map();
@@ -11705,7 +12859,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11705
12859
  };
11706
12860
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
11707
12861
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
11708
- const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
12862
+ const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
11709
12863
  return json({ meetings, adopted: decisions.length, timezone });
11710
12864
  };
11711
12865
 
@@ -11730,10 +12884,10 @@ async function repairAccountPeoplePage(deps, after) {
11730
12884
  }
11731
12885
 
11732
12886
  // src/crm-application-repair.ts
11733
- var import_crm14 = require("@odla-ai/crm");
12887
+ var import_crm15 = require("@odla-ai/crm");
11734
12888
  async function repairApplicationLink(db, chapter, recordId, actorId) {
11735
12889
  const deps = { crm: chapter.crm, db };
11736
- const record4 = await (0, import_crm14.getRecord)(deps, recordId);
12890
+ const record4 = await (0, import_crm15.getRecord)(deps, recordId);
11737
12891
  if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
11738
12892
  if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
11739
12893
  const fields = record4.fields;
@@ -11751,7 +12905,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
11751
12905
  }
11752
12906
  const app = [...applications.values()][0];
11753
12907
  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, {
12908
+ await (0, import_crm15.updateRecord)(deps, {
11755
12909
  id: recordId,
11756
12910
  input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
11757
12911
  expectedUpdatedAt: record4.updatedAt,
@@ -11844,15 +12998,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11844
12998
  for (const a of data.applications ?? []) {
11845
12999
  const key3 = String(a.email ?? "").toLowerCase();
11846
13000
  if (!key3) continue;
11847
- const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
13001
+ const name2 = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
11848
13002
  const row2 = people.get(key3);
11849
13003
  if (row2) {
11850
13004
  if (!row2.application) row2.application = applicationSummary(a);
11851
- if (!row2.name) row2.name = name;
13005
+ if (!row2.name) row2.name = name2;
11852
13006
  } else {
11853
13007
  people.set(key3, {
11854
13008
  email: String(a.email),
11855
- name,
13009
+ name: name2,
11856
13010
  userId: null,
11857
13011
  role: null,
11858
13012
  profile: null,
@@ -11861,10 +13015,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11861
13015
  });
11862
13016
  }
11863
13017
  }
11864
- const rows5 = [...people.values()].sort(
13018
+ const rows6 = [...people.values()].sort(
11865
13019
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
11866
13020
  );
11867
- return json({ people: rows5 });
13021
+ return json({ people: rows6 });
11868
13022
  };
11869
13023
  var handleAdminPeopleAccess = async (req, url, env, ctx) => {
11870
13024
  if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
@@ -12065,7 +13219,7 @@ async function gate3(req, env, ctx) {
12065
13219
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
12066
13220
  return rawDb;
12067
13221
  }
12068
- var rows4 = (v) => Array.isArray(v) ? v : [];
13222
+ var rows5 = (v) => Array.isArray(v) ? v : [];
12069
13223
  var handleAdminDashboard = async (req, url, env, ctx) => {
12070
13224
  if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
12071
13225
  const got = await gate3(req, env, ctx);
@@ -12087,8 +13241,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12087
13241
  crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
12088
13242
  groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
12089
13243
  });
12090
- const apps = rows4(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
12091
- const incomplete = rows4(data.applications).length >= 1e3 || rows4(data.crm_record).length >= 1e3 || rows4(data.meetings).length >= 500;
13244
+ const apps = rows5(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
13245
+ const incomplete = rows5(data.applications).length >= 1e3 || rows5(data.crm_record).length >= 1e3 || rows5(data.meetings).length >= 500;
12092
13246
  const applications = {
12093
13247
  total: apps.length,
12094
13248
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -12104,7 +13258,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12104
13258
  for (const a of apps) {
12105
13259
  if (String(a.status) in pipeline) pipeline[String(a.status)] = (pipeline[String(a.status)] ?? 0) + 1;
12106
13260
  }
12107
- for (const r of rows4(data.crm_record)) {
13261
+ for (const r of rows5(data.crm_record)) {
12108
13262
  if (!appById.has(r.fields?.applicationId)) continue;
12109
13263
  const s = r.stage;
12110
13264
  if (s in pipeline) {
@@ -12112,7 +13266,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12112
13266
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
12113
13267
  }
12114
13268
  }
12115
- const meetings = rows4(data.meetings).filter((m) => appById.has(m.applicationId));
13269
+ const meetings = rows5(data.meetings).filter((m) => appById.has(m.applicationId));
12116
13270
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
12117
13271
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
12118
13272
  const agenda = upcoming.slice(0, 8).map((m) => {
@@ -12135,12 +13289,12 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12135
13289
  const sk = await getVaultSecret(db, "stripe_secret_key");
12136
13290
  if (sk) {
12137
13291
  const subsRes = await chapterBillingSubscriptions(sk, ctx.chapter.id, env).catch(() => null);
12138
- if (subsRes?.complete && rows4(data.applications).length < 1e3) {
13292
+ if (subsRes?.complete && rows5(data.applications).length < 1e3) {
12139
13293
  const subs = subsRes.objects.filter((s) => {
12140
13294
  const a = appById.get(s.metadata?.applicationId);
12141
13295
  return a?.stripeSubscriptionId === s.id && a?.stripeCustomerId === s.customer;
12142
13296
  });
12143
- const intents = new Map(rows4(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
13297
+ const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
12144
13298
  const amount = (s) => chapterBillingAmount(s, intents.get(s.metadata?.applicationId), env);
12145
13299
  const subMs = (s) => (s.created ?? 0) * 1e3;
12146
13300
  membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
@@ -12179,9 +13333,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12179
13333
  const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
12180
13334
  if (!subsRes) return json({ error: "billing lookup failed upstream" }, 502);
12181
13335
  const subById = new Map(subsRes.objects.map((s) => [s.id, s]));
12182
- const intents = new Map(rows4(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
13336
+ const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
12183
13337
  const billingRows = [];
12184
- for (const a of rows4(data.applications)) {
13338
+ for (const a of rows5(data.applications)) {
12185
13339
  if (!chapterBillingApplication(a, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
12186
13340
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
12187
13341
  const candidate = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
@@ -12217,9 +13371,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12217
13371
  return json({
12218
13372
  billingReady: true,
12219
13373
  testMode,
12220
- truncated: !subsRes.complete || rows4(data.applications).length >= 200,
13374
+ truncated: !subsRes.complete || rows5(data.applications).length >= 200,
12221
13375
  rows: billingRows,
12222
- summary: subsRes.complete && rows4(data.applications).length < 200 ? summary : null
13376
+ summary: subsRes.complete && rows5(data.applications).length < 200 ? summary : null
12223
13377
  });
12224
13378
  };
12225
13379
 
@@ -12572,10 +13726,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
12572
13726
  };
12573
13727
 
12574
13728
  // src/email-admin-config.ts
12575
- var import_crm16 = require("@odla-ai/crm");
13729
+ var import_crm17 = require("@odla-ai/crm");
12576
13730
 
12577
13731
  // src/email-readiness.ts
12578
- var import_crm15 = require("@odla-ai/crm");
13732
+ var import_crm16 = require("@odla-ai/crm");
12579
13733
 
12580
13734
  // src/email-spam-signals.ts
12581
13735
  var TRIGGER_PHRASES = [
@@ -12611,7 +13765,8 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
12611
13765
  var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
12612
13766
  function emailSpamSignals(copy) {
12613
13767
  const subject = typeof copy.subject === "string" ? copy.subject : "";
12614
- const text10 = typeof copy.text === "string" ? copy.text : "";
13768
+ const text11 = typeof copy.text === "string" ? copy.text : "";
13769
+ const html = typeof copy.html === "string" ? copy.html : "";
12615
13770
  const out = [];
12616
13771
  const letters = strip(subject).replace(/[^A-Za-z]/g, "");
12617
13772
  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, "")));
@@ -12620,11 +13775,12 @@ function emailSpamSignals(copy) {
12620
13775
  if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
12621
13776
  const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
12622
13777
  if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
12623
- const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
13778
+ const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text11));
12624
13779
  if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
12625
- 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
- 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]);
13780
+ if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text11))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
13781
+ if (/!{2,}/.test(text11)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
13782
+ const urls = [...`${text11}
13783
+ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
12628
13784
  const hosts = urls.map((url) => {
12629
13785
  try {
12630
13786
  return new URL(url).hostname;
@@ -12636,7 +13792,7 @@ function emailSpamSignals(copy) {
12636
13792
  if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
12637
13793
  if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
12638
13794
  if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
12639
- if (urls.length && words(text10).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
13795
+ if (urls.length && words(text11).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
12640
13796
  return out;
12641
13797
  }
12642
13798
  function emailSenderWarnings(input) {
@@ -12661,19 +13817,23 @@ function validateEmailReadiness(input) {
12661
13817
  }
12662
13818
  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
13819
  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.` });
13820
+ 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
13821
  }
12666
- const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm15.isEmailTierName)(tier.name, tier.id));
13822
+ const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm16.isEmailTierName)(tier.name, tier.id));
12667
13823
  if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
12668
13824
  const events = Object.entries(input.events).map(([key3, event2]) => {
12669
13825
  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 });
13826
+ const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm16.validateEmailCopy)(template, { allowedVariables: event2.variables });
12671
13827
  if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
13828
+ const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
13829
+ if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
12672
13830
  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()) {
12676
- problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
13831
+ const copy = `${template.subject ?? ""}
13832
+ ${template.text ?? ""}
13833
+ ${html}`;
13834
+ const sectionOnly = new Set((0, import_crm16.templateSectionNames)(copy));
13835
+ for (const name2 of (0, import_crm16.templateVars)(copy)) if (name2 in input.groupValues && !input.groupValues[name2]?.trim() && !sectionOnly.has(name2)) {
13836
+ problems.push({ code: "value-missing", variable: name2, message: `Add the ${name2} used by this email.` });
12677
13837
  }
12678
13838
  }
12679
13839
  if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
@@ -12694,13 +13854,14 @@ var str2 = (value) => typeof value === "string" ? value : "";
12694
13854
  async function emailSettings(db, ctx, env) {
12695
13855
  const hub = ctx.chapter.mode === "hub";
12696
13856
  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];
13857
+ const group = hub ? await (0, import_crm17.getConfig)(crmDeps3) : (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
12698
13858
  const raw2 = group?.emailTemplates;
12699
13859
  const stored = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
12700
13860
  const events = chapterEmailEvents(ctx.chapter);
12701
13861
  const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
12702
13862
  subject: str2(stored[key3]?.subject),
12703
13863
  text: str2(stored[key3]?.text),
13864
+ html: str2(stored[key3]?.html),
12704
13865
  enabled: stored[key3]?.enabled
12705
13866
  }]));
12706
13867
  let tiers = [], tiersUnavailable = false;
@@ -12741,18 +13902,20 @@ async function updateEmailSettings(db, ctx, env, body) {
12741
13902
  if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
12742
13903
  const templates = body.emailTemplates;
12743
13904
  if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
12744
- const clean = {};
13905
+ const clean2 = {};
12745
13906
  for (const [key3, value] of Object.entries(templates)) {
12746
13907
  if (!events[key3]) continue;
12747
13908
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12748
13909
  const t = value;
12749
13910
  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);
13911
+ const subject = str2(t.subject).trim(), text11 = str2(t.text), html = str2(t.html);
13912
+ if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
13913
+ const issues = (0, import_crm17.validateEmailCopy)({ subject, text: text11 }, { allowedVariables: events[key3].variables });
13914
+ const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
13915
+ if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
13916
+ return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
12754
13917
  }
12755
- clean[key3] = { subject, text: text10, enabled: t.enabled };
13918
+ clean2[key3] = { subject, text: text11, ...html.trim() ? { html } : {}, enabled: t.enabled };
12756
13919
  }
12757
13920
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12758
13921
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -12765,20 +13928,20 @@ async function updateEmailSettings(db, ctx, env, body) {
12765
13928
  if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
12766
13929
  if (ctx.chapter.mode === "hub") {
12767
13930
  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 });
13931
+ await (0, import_crm17.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean2 }, notificationEmail, replyTo, debugEmail });
12769
13932
  } else {
12770
13933
  const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
12771
13934
  await db.transact([
12772
- { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
13935
+ { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean2 } },
12773
13936
  { t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
12774
13937
  ]);
12775
13938
  }
12776
- const warnings = Object.fromEntries(Object.entries(clean).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
13939
+ const warnings = Object.fromEntries(Object.entries(clean2).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
12777
13940
  return json({ ok: true, warnings });
12778
13941
  }
12779
13942
 
12780
13943
  // src/worker-routes-admin-comms.ts
12781
- var import_crm17 = require("@odla-ai/crm");
13944
+ var import_crm18 = require("@odla-ai/crm");
12782
13945
  async function gate5(req, env, ctx) {
12783
13946
  const rawDb = ctx.makeDb(env);
12784
13947
  const u = await ctx.verifyUser(req, env);
@@ -12811,8 +13974,8 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
12811
13974
  const got = await gate5(req, env, ctx);
12812
13975
  if (got instanceof Response) return got;
12813
13976
  if (ctx.chapter.mode === "hub") {
12814
- const rows5 = await (0, import_crm17.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
12815
- return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
13977
+ const rows6 = await (0, import_crm18.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
13978
+ return json({ sends: rows6.map((row2) => ({ ...row2, template: row2.templateId })) });
12816
13979
  }
12817
13980
  const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
12818
13981
  const visible = [];
@@ -12908,7 +14071,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
12908
14071
  };
12909
14072
 
12910
14073
  // src/worker-routes-network.ts
12911
- var import_crm19 = require("@odla-ai/crm");
14074
+ var import_crm20 = require("@odla-ai/crm");
12912
14075
  var import_db9 = require("@odla-ai/db");
12913
14076
 
12914
14077
  // src/network-edge.ts
@@ -12934,7 +14097,7 @@ function unpairedResponse(unpaired) {
12934
14097
  }
12935
14098
 
12936
14099
  // src/worker-network-push.ts
12937
- var import_crm18 = require("@odla-ai/crm");
14100
+ var import_crm19 = require("@odla-ai/crm");
12938
14101
  var import_db8 = require("@odla-ai/db");
12939
14102
  async function pushNetworkRecord(db, env, ctx, target, record4) {
12940
14103
  const secret = await getVaultSecret(db, target.secretName);
@@ -12946,7 +14109,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
12946
14109
  };
12947
14110
  if (!secret) {
12948
14111
  const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
12949
- await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
14112
+ await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
12950
14113
  recordId: record4.id,
12951
14114
  targetId: target.id,
12952
14115
  status: "failed",
@@ -12985,7 +14148,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
12985
14148
  const responseBody = await res.json().catch(() => ({}));
12986
14149
  if (!res.ok) {
12987
14150
  const error = responseBody.error ?? "follower rejected the record";
12988
- await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
14151
+ await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
12989
14152
  recordId: record4.id,
12990
14153
  targetId: target.id,
12991
14154
  status: "failed",
@@ -13000,7 +14163,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
13000
14163
  error
13001
14164
  };
13002
14165
  }
13003
- await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
14166
+ await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
13004
14167
  recordId: record4.id,
13005
14168
  targetId: target.id,
13006
14169
  status: "delivered",
@@ -13016,7 +14179,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
13016
14179
  };
13017
14180
  } catch (err) {
13018
14181
  const error = err instanceof Error ? err.message : "delivery failed";
13019
- await (0, import_crm18.recordDeliveryAttempt)(crmDeps3, {
14182
+ await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
13020
14183
  recordId: record4.id,
13021
14184
  targetId: target.id,
13022
14185
  status: "failed",
@@ -13042,7 +14205,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
13042
14205
  return json({
13043
14206
  targets: ctx.chapter.network.targets.map(({
13044
14207
  id: id7,
13045
- name,
14208
+ name: name2,
13046
14209
  url: targetUrl,
13047
14210
  fields,
13048
14211
  sharedNotes,
@@ -13051,7 +14214,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
13051
14214
  commercialParity
13052
14215
  }) => ({
13053
14216
  id: id7,
13054
- name,
14217
+ name: name2,
13055
14218
  url: targetUrl,
13056
14219
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
13057
14220
  sharedNotes,
@@ -13069,7 +14232,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
13069
14232
  const verified = await (0, import_db9.verifyFederatedRequest)(req, { secret });
13070
14233
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
13071
14234
  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))
14235
+ Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm20.typeSummary)({ crm: ctx.chapter.crm, db }, type))
13073
14236
  );
13074
14237
  const activeConfiguration = ctx.chapter.signupControl ? await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV) : null;
13075
14238
  const snapshot = {
@@ -13216,7 +14379,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
13216
14379
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
13217
14380
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
13218
14381
  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);
14382
+ const record4 = await (0, import_crm20.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
13220
14383
  if (!record4) return json({ error: "record not found" }, 404);
13221
14384
  const results = await Promise.all(
13222
14385
  targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
@@ -13225,11 +14388,11 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
13225
14388
  };
13226
14389
 
13227
14390
  // src/worker-routes-network-records-follower.ts
13228
- var import_crm23 = require("@odla-ai/crm");
14391
+ var import_crm24 = require("@odla-ai/crm");
13229
14392
  var import_db10 = require("@odla-ai/db");
13230
14393
 
13231
14394
  // src/network-application-action.ts
13232
- var import_crm20 = require("@odla-ai/crm");
14395
+ var import_crm21 = require("@odla-ai/crm");
13233
14396
  var ACTION_META = "chapterNetworkMutation";
13234
14397
  async function networkApplicationAction(opts) {
13235
14398
  const { db, env, ctx, record: record4, input, sourceId, activityId, fingerprint } = opts;
@@ -13319,7 +14482,7 @@ async function networkApplicationAction(opts) {
13319
14482
  const current = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
13320
14483
  if (!current || current.status !== input.stage) return json({ error: "application reconciliation pending", code: "action_pending" }, 503);
13321
14484
  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);
14485
+ const projected = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
13323
14486
  if (!projected || projected.stage !== input.stage) return json({ error: "CRM reconciliation pending", code: "action_pending" }, 503);
13324
14487
  const audit = (await db.query({ crm_activity: { $: { where: { id: activityId }, limit: 1 } } })).crm_activity?.[0];
13325
14488
  const priorMeta = audit?.meta;
@@ -13329,7 +14492,7 @@ async function networkApplicationAction(opts) {
13329
14492
  } },
13330
14493
  { t: "update", ns: "crm_activity", id: lockId, attrs: { meta: { state: "complete", activityId } } }
13331
14494
  ], { 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);
14495
+ const updated = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
13333
14496
  if (!updated) return json({ error: "record not found" }, 404);
13334
14497
  return { record: updated, duplicate: Boolean(receipt) };
13335
14498
  }
@@ -13365,7 +14528,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
13365
14528
  }
13366
14529
 
13367
14530
  // src/worker-routes-network-records-search.ts
13368
- var import_crm21 = require("@odla-ai/crm");
14531
+ var import_crm22 = require("@odla-ai/crm");
13369
14532
  var SEARCH_BATCH_SIZE = 1e3;
13370
14533
  var MAX_READER_SEARCH_POPULATION = 1e4;
13371
14534
  var ReaderSearchPopulationError = class extends Error {
@@ -13385,8 +14548,8 @@ function terms(value) {
13385
14548
  function matchesReaderIdentity(record4, search, includeEmail) {
13386
14549
  const query = terms(search);
13387
14550
  if (!query.length) return true;
13388
- const name = folded(record4.name);
13389
- const visible = [name, ...name.split(/\s+/u).filter(Boolean)];
14551
+ const name2 = folded(record4.name);
14552
+ const visible = [name2, ...name2.split(/\s+/u).filter(Boolean)];
13390
14553
  if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
13391
14554
  return query.every((term) => visible.some((candidate) => candidate.includes(term)));
13392
14555
  }
@@ -13406,23 +14569,23 @@ async function listVisibleSearchPopulation(db, where, window, count) {
13406
14569
  (_, index2) => index2 * SEARCH_BATCH_SIZE
13407
14570
  );
13408
14571
  const pages = await Promise.all(offsets.map((offset) => db.query({
13409
- [import_crm21.CRM_NS.record]: { $: {
14572
+ [import_crm22.CRM_NS.record]: { $: {
13410
14573
  where,
13411
14574
  order: { [window.sort.field]: window.sort.dir },
13412
14575
  limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
13413
14576
  offset
13414
14577
  } }
13415
14578
  })));
13416
- return pages.flatMap((page) => (page[import_crm21.CRM_NS.record] ?? []).map(import_crm21.rowToRecord));
14579
+ return pages.flatMap((page) => (page[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord));
13417
14580
  }
13418
- async function listReaderRecords(db, type, allowed2, emailField, window) {
14581
+ async function listReaderRecords(db, type, allowed, emailField, window) {
13419
14582
  const where = baseWhere(type, window.stage);
13420
14583
  const crmDb = db;
13421
- const aggregate = await crmDb.aggregate(import_crm21.CRM_NS.record, { count: true }, { where });
14584
+ const aggregate = await crmDb.aggregate(import_crm22.CRM_NS.record, { count: true }, { where });
13422
14585
  const candidateCount = aggregate.count ?? 0;
13423
14586
  if (window.search) {
13424
14587
  const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
13425
- const includeEmail = Boolean(emailField && allowed2.includes(emailField));
14588
+ const includeEmail = Boolean(emailField && allowed.includes(emailField));
13426
14589
  const matches2 = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
13427
14590
  return {
13428
14591
  records: matches2.slice(window.offset, window.offset + window.limit),
@@ -13430,7 +14593,7 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
13430
14593
  };
13431
14594
  }
13432
14595
  const result = await crmDb.query({
13433
- [import_crm21.CRM_NS.record]: { $: {
14596
+ [import_crm22.CRM_NS.record]: { $: {
13434
14597
  where,
13435
14598
  order: { [window.sort.field]: window.sort.dir },
13436
14599
  limit: window.limit,
@@ -13438,19 +14601,19 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
13438
14601
  } }
13439
14602
  });
13440
14603
  return {
13441
- records: (result[import_crm21.CRM_NS.record] ?? []).map(import_crm21.rowToRecord),
14604
+ records: (result[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord),
13442
14605
  total: candidateCount
13443
14606
  };
13444
14607
  }
13445
14608
 
13446
14609
  // src/network-mutation-receipt.ts
13447
- var import_crm22 = require("@odla-ai/crm");
14610
+ var import_crm23 = require("@odla-ai/crm");
13448
14611
  var NETWORK_MUTATION_META = "chapterNetworkMutation";
13449
14612
  async function priorMutation(db, id7) {
13450
14613
  const result = await db.query({
13451
- [import_crm22.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
14614
+ [import_crm23.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
13452
14615
  });
13453
- const row2 = (result[import_crm22.CRM_NS.activity] ?? [])[0];
14616
+ const row2 = (result[import_crm23.CRM_NS.activity] ?? [])[0];
13454
14617
  const meta = row2?.meta;
13455
14618
  const receipt = meta?.[NETWORK_MUTATION_META];
13456
14619
  return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint, pending: receipt.state === "pending", failed: receipt.state === "failed" } : null;
@@ -13474,7 +14637,7 @@ function mutationError(error) {
13474
14637
  if (error?.code === "transact_guard_failed") {
13475
14638
  return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
13476
14639
  }
13477
- if (error instanceof import_crm23.CrmInputError) {
14640
+ if (error instanceof import_crm24.CrmInputError) {
13478
14641
  return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
13479
14642
  }
13480
14643
  return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
@@ -13515,7 +14678,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13515
14678
  const editable = reader.editableFields[input.type];
13516
14679
  if (hasFields) {
13517
14680
  const names = Object.keys(fields);
13518
- if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
14681
+ if (!editable || names.length === 0 || names.length > 32 || names.some((name2) => !editable.includes(name2))) {
13519
14682
  return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
13520
14683
  }
13521
14684
  } else if (!reader.stageTransitions.includes(input.type)) {
@@ -13529,7 +14692,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13529
14692
  }
13530
14693
  if (prior?.failed) return json({ error: "this operation was not applied; reload before choosing a new action", code: "action_failed" }, 409);
13531
14694
  if (prior && !prior.pending) {
13532
- const replayed = await (0, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
14695
+ const replayed = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
13533
14696
  if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
13534
14697
  const result = {
13535
14698
  version: 1,
@@ -13539,7 +14702,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13539
14702
  };
13540
14703
  return json(result);
13541
14704
  }
13542
- const record4 = await (0, import_crm23.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
14705
+ const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
13543
14706
  if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
13544
14707
  if (!prior?.pending && record4.updatedAt !== input.expectedRevision) {
13545
14708
  return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
@@ -13580,7 +14743,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13580
14743
  });
13581
14744
  }
13582
14745
  const capabilities = await operationalReaderRecord(record4, reader, db, env, ctx);
13583
- if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
14746
+ if (hasFields && Object.keys(fields).some((name2) => !capabilities.edit?.fields.some((field) => field.id === name2))) {
13584
14747
  return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
13585
14748
  }
13586
14749
  const deps = { crm: ctx.chapter.crm, db };
@@ -13602,7 +14765,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13602
14765
  record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
13603
14766
  });
13604
14767
  }
13605
- const updated = hasFields ? await (0, import_crm23.updateRecord)(deps, {
14768
+ const updated = hasFields ? await (0, import_crm24.updateRecord)(deps, {
13606
14769
  id: record4.id,
13607
14770
  input: fields,
13608
14771
  mutationId: activityId,
@@ -13613,7 +14776,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13613
14776
  authorId: `network:${verified.sender}`,
13614
14777
  meta: auditMeta
13615
14778
  }
13616
- }) : await (0, import_crm23.setStage)(deps, {
14779
+ }) : await (0, import_crm24.setStage)(deps, {
13617
14780
  id: record4.id,
13618
14781
  to: input.stage,
13619
14782
  authorId: `network:${verified.sender}`,
@@ -13635,12 +14798,12 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13635
14798
  }
13636
14799
  }
13637
14800
  const type = url.searchParams.get("type") ?? "";
13638
- const allowed2 = reader.fields[type];
13639
- if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
14801
+ const allowed = reader.fields[type];
14802
+ if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
13640
14803
  const recordId = url.searchParams.get("recordId");
13641
14804
  if (recordId !== null) {
13642
14805
  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);
14806
+ const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
13644
14807
  if (!record4 || record4.type !== type || record4.archived) return json({ error: "record not found" }, 404);
13645
14808
  return json({
13646
14809
  version: 1,
@@ -13664,7 +14827,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13664
14827
  page = await listReaderRecords(
13665
14828
  db,
13666
14829
  type,
13667
- allowed2,
14830
+ allowed,
13668
14831
  typeDef.emailField,
13669
14832
  window
13670
14833
  );
@@ -13745,8 +14908,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
13745
14908
  async function mutateTargetRecord(db, env, ctx, target, input) {
13746
14909
  const fields = input.fields;
13747
14910
  if (fields) {
13748
- const allowed2 = target.editableFields[input.type];
13749
- if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
14911
+ const allowed = target.editableFields[input.type];
14912
+ if (!allowed || Object.keys(fields).some((name2) => !allowed.includes(name2))) {
13750
14913
  return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
13751
14914
  }
13752
14915
  } else if (!target.stageTransitions.includes(input.type)) {
@@ -13935,7 +15098,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
13935
15098
  };
13936
15099
 
13937
15100
  // src/worker-routes-network-shared-notes.ts
13938
- var import_crm24 = require("@odla-ai/crm");
15101
+ var import_crm25 = require("@odla-ai/crm");
13939
15102
  var import_db12 = require("@odla-ai/db");
13940
15103
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
13941
15104
  var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
@@ -13966,7 +15129,7 @@ function sharedNote(activity, sourceId) {
13966
15129
  }
13967
15130
  async function listSharedNotes(db, sourceId, recordId) {
13968
15131
  const result = await db.query({
13969
- [import_crm24.CRM_NS.activity]: {
15132
+ [import_crm25.CRM_NS.activity]: {
13970
15133
  $: {
13971
15134
  where: {
13972
15135
  and: [
@@ -13979,7 +15142,7 @@ async function listSharedNotes(db, sourceId, recordId) {
13979
15142
  }
13980
15143
  }
13981
15144
  });
13982
- return (result[import_crm24.CRM_NS.activity] ?? []).flatMap((activity) => {
15145
+ return (result[import_crm25.CRM_NS.activity] ?? []).flatMap((activity) => {
13983
15146
  const note = sharedNote(activity, sourceId);
13984
15147
  return note ? [note] : [];
13985
15148
  });
@@ -14007,7 +15170,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
14007
15170
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
14008
15171
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
14009
15172
  }
14010
- const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
15173
+ const record4 = await (0, import_crm25.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
14011
15174
  if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
14012
15175
  if (req.method === "GET") {
14013
15176
  return json({
@@ -14029,7 +15192,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
14029
15192
  }
14030
15193
  const createdAt = Date.now();
14031
15194
  const id7 = `network:${verified.sender}:${mutationId}`;
14032
- const result = await (0, import_crm24.addActivity)(
15195
+ const result = await (0, import_crm25.addActivity)(
14033
15196
  {
14034
15197
  crm: ctx.chapter.crm,
14035
15198
  db,
@@ -14590,20 +15753,20 @@ async function networkPersonBulkSha256(value) {
14590
15753
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
14591
15754
  return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
14592
15755
  }
14593
- function assertNetworkPersonBulkRows(rows5) {
14594
- if (!Array.isArray(rows5) || rows5.length < 1) {
15756
+ function assertNetworkPersonBulkRows(rows6) {
15757
+ if (!Array.isArray(rows6) || rows6.length < 1) {
14595
15758
  throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
14596
15759
  }
14597
- if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
15760
+ if (rows6.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
14598
15761
  throw new NetworkPersonRegistryError(
14599
15762
  "bulk_rows_exceeded",
14600
15763
  `bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
14601
15764
  );
14602
15765
  }
14603
- return rows5;
15766
+ return rows6;
14604
15767
  }
14605
- async function networkPersonBulkBatchDigest(rows5) {
14606
- const bounded = assertNetworkPersonBulkRows(rows5);
15768
+ async function networkPersonBulkBatchDigest(rows6) {
15769
+ const bounded = assertNetworkPersonBulkRows(rows6);
14607
15770
  return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
14608
15771
  }
14609
15772
  async function networkPersonRegistryStateDigest(state) {
@@ -14618,7 +15781,7 @@ async function networkPersonRegistryStateDigest(state) {
14618
15781
  function rejectedValidation(errors) {
14619
15782
  return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
14620
15783
  }
14621
- async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
15784
+ async function classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state) {
14622
15785
  const aliasIds = new Set(state.aliases.map((alias) => alias.id));
14623
15786
  const batchAliases = /* @__PURE__ */ new Set();
14624
15787
  const claimOwners = /* @__PURE__ */ new Map();
@@ -14629,8 +15792,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14629
15792
  }
14630
15793
  const results = [];
14631
15794
  const values = [];
14632
- for (let index2 = 0; index2 < rows5.length; index2 += 1) {
14633
- const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
15795
+ for (let index2 = 0; index2 < rows6.length; index2 += 1) {
15796
+ const validation = validateNetworkPersonImport(rows6[index2], allowedChapterIds);
14634
15797
  if (!validation.ok || !validation.value) {
14635
15798
  const errors = validation.errors.slice(0, 8);
14636
15799
  results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
@@ -14663,8 +15826,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14663
15826
  }
14664
15827
  return { rows: results, values };
14665
15828
  }
14666
- async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
14667
- const bounded = assertNetworkPersonBulkRows(rows5);
15829
+ async function previewNetworkPersonBulkImport(db, rows6, allowedChapterIds) {
15830
+ const bounded = assertNetworkPersonBulkRows(rows6);
14668
15831
  const state = await loadNetworkPersonRegistry(db);
14669
15832
  const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
14670
15833
  const counts = networkPersonBulkEmptyCounts();
@@ -14695,8 +15858,8 @@ function eventCounts(event2) {
14695
15858
  }
14696
15859
  async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
14697
15860
  if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
14698
- const rows5 = assertNetworkPersonBulkRows(input.rows);
14699
- const batchDigest = await networkPersonBulkBatchDigest(rows5);
15861
+ const rows6 = assertNetworkPersonBulkRows(input.rows);
15862
+ const batchDigest = await networkPersonBulkBatchDigest(rows6);
14700
15863
  if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
14701
15864
  throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
14702
15865
  }
@@ -14715,7 +15878,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14715
15878
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14716
15879
  batchDigest,
14717
15880
  stateDigest: eventString(completed, "stateDigest"),
14718
- rowCount: Number(completed.detail.rowCount) || rows5.length,
15881
+ rowCount: Number(completed.detail.rowCount) || rows6.length,
14719
15882
  counts: eventCounts(completed),
14720
15883
  rows: [],
14721
15884
  replayed: true
@@ -14723,7 +15886,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14723
15886
  }
14724
15887
  let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
14725
15888
  if (!started) {
14726
- const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
15889
+ const preview = await previewNetworkPersonBulkImport(db, rows6, allowedChapterIds);
14727
15890
  if (preview.stateDigest !== input.expectedStateDigest) {
14728
15891
  throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
14729
15892
  }
@@ -14732,7 +15895,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14732
15895
  networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
14733
15896
  batchDigest,
14734
15897
  stateDigest: input.expectedStateDigest,
14735
- rowCount: rows5.length,
15898
+ rowCount: rows6.length,
14736
15899
  previewCounts: preview.counts
14737
15900
  })
14738
15901
  ], {
@@ -14748,7 +15911,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14748
15911
  if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
14749
15912
  throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
14750
15913
  }
14751
- const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
15914
+ const classified = await classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state);
14752
15915
  const results = [];
14753
15916
  const counts = networkPersonBulkEmptyCounts();
14754
15917
  for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
@@ -14787,10 +15950,10 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14787
15950
  }
14788
15951
  if (counts.failed === 0) {
14789
15952
  await db.transact([
14790
- networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
15953
+ networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows6.length + 1, {
14791
15954
  batchDigest,
14792
15955
  stateDigest: input.expectedStateDigest,
14793
- rowCount: rows5.length,
15956
+ rowCount: rows6.length,
14794
15957
  counts
14795
15958
  })
14796
15959
  ], { mutationId: completedMutationId });
@@ -14799,7 +15962,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14799
15962
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14800
15963
  batchDigest,
14801
15964
  stateDigest: input.expectedStateDigest,
14802
- rowCount: rows5.length,
15965
+ rowCount: rows6.length,
14803
15966
  counts,
14804
15967
  rows: results
14805
15968
  };
@@ -14920,7 +16083,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
14920
16083
  }
14921
16084
 
14922
16085
  // src/worker-routes-network-person-registry.ts
14923
- var PATH6 = "/api/admin/network-person-registry";
16086
+ var PATH7 = "/api/admin/network-person-registry";
14924
16087
  var MAX_BODY2 = 8 * 1024 * 1024;
14925
16088
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
14926
16089
  var positiveRevision = (value) => {
@@ -14945,7 +16108,7 @@ function registryError(error) {
14945
16108
  return null;
14946
16109
  }
14947
16110
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14948
- if (url.pathname !== PATH6) return null;
16111
+ if (url.pathname !== PATH7) return null;
14949
16112
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
14950
16113
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
14951
16114
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -14957,9 +16120,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14957
16120
  if (req.method === "POST") {
14958
16121
  let body;
14959
16122
  try {
14960
- const text10 = await req.text();
14961
- if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14962
- const parsed = JSON.parse(text10);
16123
+ const text11 = await req.text();
16124
+ if (new TextEncoder().encode(text11).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
16125
+ const parsed = JSON.parse(text11);
14963
16126
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14964
16127
  body = parsed;
14965
16128
  } catch {
@@ -15128,7 +16291,7 @@ var import_db13 = require("@odla-ai/db");
15128
16291
  // src/network-commercial-parity.ts
15129
16292
  var DIGEST = /^[0-9a-f]{64}$/;
15130
16293
  var MAX_RECORDS = 500;
15131
- var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
16294
+ var text9 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
15132
16295
  function parityMessage(partition, kind, value) {
15133
16296
  return [
15134
16297
  "chapter-commercial-parity",
@@ -15160,18 +16323,18 @@ async function digestWithKey(key3, partition, kind, value) {
15160
16323
  async function buildChapterCommercialParitySnapshot(input) {
15161
16324
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
15162
16325
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
15163
- const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
16326
+ const eligible = input.rows.filter((row2) => text9(row2.id) && (text9(row2.stripeCustomerId) || text9(row2.stripeSubscriptionId)));
15164
16327
  const selected2 = eligible.slice(0, maxRecords);
15165
16328
  const key3 = await parityKey(input.secret);
15166
16329
  const records = await Promise.all(selected2.map(async (row2) => {
15167
- const applicationId = text8(row2.id);
15168
- const customerId = text8(row2.stripeCustomerId);
15169
- const subscriptionId = text8(row2.stripeSubscriptionId);
16330
+ const applicationId = text9(row2.id);
16331
+ const customerId = text9(row2.stripeCustomerId);
16332
+ const subscriptionId = text9(row2.stripeSubscriptionId);
15170
16333
  return {
15171
16334
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
15172
16335
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
15173
16336
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
15174
- status: text8(row2.status, 64),
16337
+ status: text9(row2.status, 64),
15175
16338
  canceled: row2.canceled === true
15176
16339
  };
15177
16340
  }));
@@ -15197,7 +16360,7 @@ function validSnapshot(snapshot, source) {
15197
16360
  if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
15198
16361
  const seen = /* @__PURE__ */ new Set();
15199
16362
  for (const record4 of records) {
15200
- if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text8(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
16363
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text9(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
15201
16364
  const key3 = recordKey(record4);
15202
16365
  if (seen.has(key3)) return false;
15203
16366
  seen.add(key3);
@@ -15208,16 +16371,16 @@ async function reconcileCommercialParitySnapshot(input) {
15208
16371
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
15209
16372
  const sourceKey = networkCommercialSourceKey(input.source);
15210
16373
  const key3 = await parityKey(input.secret);
15211
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
15212
- const customerId = text8(row2.customerId);
15213
- const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
16374
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text9(row2.applicationId) && text9(row2.objectId)).map(async (row2) => {
16375
+ const customerId = text9(row2.customerId);
16376
+ const subscriptionId = text9(row2.subscriptionId) ?? text9(row2.objectId);
15214
16377
  return {
15215
16378
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
15216
16379
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
15217
16380
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
15218
16381
  status: null,
15219
16382
  canceled: false,
15220
- stripeStatus: text8(row2.status, 64),
16383
+ stripeStatus: text9(row2.status, 64),
15221
16384
  stripeCanceled: row2.status === "canceled"
15222
16385
  };
15223
16386
  }));
@@ -15299,14 +16462,14 @@ var PAGE2 = 500;
15299
16462
  var MAX_SCAN = 5e3;
15300
16463
  var MAX_RECORDS2 = 500;
15301
16464
  async function localCommercialRows(db) {
15302
- const rows5 = [];
16465
+ const rows6 = [];
15303
16466
  for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
15304
16467
  const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
15305
16468
  const page = Array.isArray(result.applications) ? result.applications : [];
15306
- rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
15307
- if (page.length < PAGE2) return { rows: rows5, truncated: false };
16469
+ rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
16470
+ if (page.length < PAGE2) return { rows: rows6, truncated: false };
15308
16471
  }
15309
- return { rows: rows5, truncated: true };
16472
+ return { rows: rows6, truncated: true };
15310
16473
  }
15311
16474
  var handleNetworkCommercialParity = async (req, url, env, ctx) => {
15312
16475
  if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
@@ -15441,8 +16604,8 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15441
16604
  const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
15442
16605
  return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
15443
16606
  }
15444
- const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15445
- const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
16607
+ const rows6 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
16608
+ const member = rows6.find((row2) => disputedChargeCoversMembership(charge, row2));
15446
16609
  if (!member) return { disposition: "not_applicable" };
15447
16610
  const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
15448
16611
  return { disposition: "applied", membershipId: receipt.membershipId };
@@ -15658,7 +16821,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
15658
16821
  };
15659
16822
 
15660
16823
  // src/worker-routes-formation.ts
15661
- var import_crm25 = require("@odla-ai/crm");
16824
+ var import_crm26 = require("@odla-ai/crm");
15662
16825
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
15663
16826
  var handleFormation = async (req, url, env, ctx) => {
15664
16827
  const formation = ctx.chapter.formation;
@@ -15686,15 +16849,15 @@ var handleFormation = async (req, url, env, ctx) => {
15686
16849
  if (submissionId && !SUBMISSION.test(submissionId)) {
15687
16850
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
15688
16851
  }
15689
- const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
15690
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
16852
+ const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
16853
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
15691
16854
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
15692
16855
  for (const field of formation.required) {
15693
16856
  const value = body[field];
15694
16857
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
15695
16858
  }
15696
16859
  const input = {};
15697
- for (const field of allowed2) {
16860
+ for (const field of allowed) {
15698
16861
  const value = body[field];
15699
16862
  if (value === void 0) continue;
15700
16863
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -15710,7 +16873,7 @@ var handleFormation = async (req, url, env, ctx) => {
15710
16873
  const db = ctx.makeDb(env);
15711
16874
  const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
15712
16875
  try {
15713
- const created = await (0, import_crm25.createRecord)(
16876
+ const created = await (0, import_crm26.createRecord)(
15714
16877
  {
15715
16878
  crm: ctx.chapter.crm,
15716
16879
  db,
@@ -15758,10 +16921,10 @@ var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
15758
16921
  var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
15759
16922
  var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
15760
16923
  var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
15761
- function unknownKeys3(value, allowed2, path, errors) {
15762
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
16924
+ function unknownKeys3(value, allowed, path, errors) {
16925
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
15763
16926
  }
15764
- function text9(value, path, errors, max = MAX_TEXT2, required = true) {
16927
+ function text10(value, path, errors, max = MAX_TEXT2, required = true) {
15765
16928
  if (value === void 0 && !required) return void 0;
15766
16929
  if (typeof value !== "string" || required && !value.trim()) {
15767
16930
  errors.push(`${path} must be a non-empty string`);
@@ -15787,7 +16950,7 @@ function scopeValue(value, path, errors) {
15787
16950
  return { kind: raw2.kind };
15788
16951
  }
15789
16952
  if (raw2.kind === "chapter") {
15790
- const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
16953
+ const chapterId = text10(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15791
16954
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15792
16955
  return { kind: "chapter", chapterId };
15793
16956
  }
@@ -15801,22 +16964,22 @@ function resourceValue(value, path, errors) {
15801
16964
  return null;
15802
16965
  }
15803
16966
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15804
- const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
16967
+ const id7 = text10(raw2.id, `${path}.id`, errors, 80) ?? "";
15805
16968
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15806
- const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
16969
+ const kind = text10(raw2.kind, `${path}.kind`, errors) ?? "";
15807
16970
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15808
- const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
15809
- const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
15810
- const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
16971
+ const title = text10(raw2.title, `${path}.title`, errors, 240) ?? "";
16972
+ const summary = text10(raw2.summary, `${path}.summary`, errors, 1e3, false);
16973
+ const status2 = text10(raw2.status, `${path}.status`, errors) ?? "";
15811
16974
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15812
16975
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15813
16976
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15814
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
16977
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text10(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15815
16978
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15816
16979
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15817
16980
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15818
- const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15819
- const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
16981
+ const bodyMarkdown = text10(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
16982
+ const url = text10(raw2.url, `${path}.url`, errors, 2048, false);
15820
16983
  if (url) {
15821
16984
  try {
15822
16985
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15828,9 +16991,9 @@ function resourceValue(value, path, errors) {
15828
16991
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15829
16992
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15830
16993
  const brandAsset = asset ? {
15831
- bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15832
- assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15833
- ...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
16994
+ bookId: text10(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
16995
+ assetId: text10(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
16996
+ ...asset.filename === void 0 ? {} : { filename: text10(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15834
16997
  } : void 0;
15835
16998
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15836
16999
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15845,17 +17008,17 @@ function resourceValue(value, path, errors) {
15845
17008
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15846
17009
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15847
17010
  const promotedFrom = promotedRaw ? {
15848
- chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15849
- resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
17011
+ chapterId: text10(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
17012
+ resourceId: text10(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15850
17013
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15851
17014
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15852
- promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
17015
+ promotedBy: text10(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15853
17016
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15854
17017
  } : void 0;
15855
17018
  const provenance2 = {
15856
- createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
17019
+ createdBy: text10(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15857
17020
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15858
- updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
17021
+ updatedBy: text10(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15859
17022
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15860
17023
  ...promotedFrom ? { promotedFrom } : {}
15861
17024
  };
@@ -15874,7 +17037,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15874
17037
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15875
17038
  const sourceType = sourceRaw?.type;
15876
17039
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15877
- const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
17040
+ const sourceAppId = text10(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15878
17041
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15879
17042
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15880
17043
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15882,8 +17045,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15882
17045
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15883
17046
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15884
17047
  const target = targetRaw ? {
15885
- appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15886
- environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
17048
+ appId: text10(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
17049
+ environment: text10(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15887
17050
  } : void 0;
15888
17051
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15889
17052
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15971,17 +17134,17 @@ async function builderLibraryDigest(document) {
15971
17134
  var control2 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
15972
17135
  var local3 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
15973
17136
  async function loadActiveBuilderLibraryControls(db, targetId, environment) {
15974
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
15975
- return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
17137
+ const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
17138
+ return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
15976
17139
  }
15977
17140
  async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
15978
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
15979
- return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
17141
+ const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
17142
+ return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
15980
17143
  }
15981
17144
  async function loadLocalBuilderLibrary(db, appId, environment) {
15982
17145
  const id7 = `builder-library-local:${environment}`;
15983
- const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
15984
- return local3(Array.isArray(rows5) ? rows5[0] : void 0);
17146
+ const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
17147
+ return local3(Array.isArray(rows6) ? rows6[0] : void 0);
15985
17148
  }
15986
17149
  async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
15987
17150
  const { document, digest } = envelope;
@@ -17624,8 +18787,8 @@ var readable = /* @__PURE__ */ new Set([
17624
18787
  "networkCommercialEvents",
17625
18788
  "networkCommercialExceptions"
17626
18789
  ]);
17627
- var check = (ns, allowed2) => {
17628
- if (!allowed2.has(ns)) throw new TypeError("namespace outside membership authority contract");
18790
+ var check = (ns, allowed) => {
18791
+ if (!allowed.has(ns)) throw new TypeError("namespace outside membership authority contract");
17629
18792
  return physical(ns);
17630
18793
  };
17631
18794
  function createMembershipAuthorityStorage(db, secretNames) {
@@ -17651,9 +18814,9 @@ function createMembershipAuthorityStorage(db, secretNames) {
17651
18814
  const guards = opts?.guards?.map((guard) => ({ ...guard, ns: check(guard.ns, readable) }));
17652
18815
  return db.transact(mapped, { ...opts, ...guards ? { guards } : {} });
17653
18816
  },
17654
- secrets: { get(name) {
17655
- if (!secrets.has(name)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
17656
- return db.secrets.get(name);
18817
+ secrets: { get(name2) {
18818
+ if (!secrets.has(name2)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
18819
+ return db.secrets.get(name2);
17657
18820
  } }
17658
18821
  };
17659
18822
  }
@@ -17823,7 +18986,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17823
18986
  const origin = new URL(options.origin), limit = options.limit ?? 100;
17824
18987
  if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash || !Number.isSafeInteger(limit) || limit < 1 || limit > 2e3) throw new TypeError("invalid approval recovery configuration");
17825
18988
  const db = ctx.makeDb(env);
17826
- const rows5 = (await db.query({ applications: { $: {
18989
+ const rows6 = (await db.query({ applications: { $: {
17827
18990
  where: {
17828
18991
  groupId: ctx.chapter.id,
17829
18992
  stripeRuntime: env.ODLA_RUNTIME ?? "default",
@@ -17834,7 +18997,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17834
18997
  limit: limit + 1
17835
18998
  } } })).applications ?? [];
17836
18999
  let repaired = 0, pending = 0;
17837
- for (const app of rows5.slice(0, limit)) {
19000
+ for (const app of rows6.slice(0, limit)) {
17838
19001
  if (app.groupId !== ctx.chapter.id || app.stripeRuntime !== (env.ODLA_RUNTIME ?? "default") || app.status !== "approved" || app.approvalEffectsPending !== true) throw new Error("approval recovery scope mismatch");
17839
19002
  try {
17840
19003
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { approvalEffectsLastAttemptAt: Date.now() } }], {
@@ -17859,7 +19022,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17859
19022
  pending++;
17860
19023
  }
17861
19024
  }
17862
- return { repaired, pending, more: rows5.length > limit };
19025
+ return { repaired, pending, more: rows6.length > limit };
17863
19026
  }
17864
19027
 
17865
19028
  // src/membership-chapter-delivery.ts
@@ -17875,7 +19038,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17875
19038
  };
17876
19039
  signal.addEventListener("abort", cancel, { once: true });
17877
19040
  let size = 0;
17878
- let text10 = "";
19041
+ let text11 = "";
17879
19042
  const decoder = new TextDecoder();
17880
19043
  try {
17881
19044
  while (true) {
@@ -17885,10 +19048,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17885
19048
  if (done) break;
17886
19049
  size += value.byteLength;
17887
19050
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17888
- text10 += decoder.decode(value, { stream: true });
19051
+ text11 += decoder.decode(value, { stream: true });
17889
19052
  }
17890
19053
  try {
17891
- return JSON.parse(text10 + decoder.decode());
19054
+ return JSON.parse(text11 + decoder.decode());
17892
19055
  } catch {
17893
19056
  throw new TypeError(`invalid ${label}`);
17894
19057
  }
@@ -17901,14 +19064,14 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17901
19064
  }
17902
19065
 
17903
19066
  // src/membership-chapter-delivery.ts
17904
- var import_crm26 = require("@odla-ai/crm");
19067
+ var import_crm27 = require("@odla-ai/crm");
17905
19068
 
17906
19069
  // src/invitation-test-delivery.ts
17907
19070
  function invitationDelivery(env, recipientEmail) {
17908
19071
  const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
17909
19072
  if (env.ODLA_ENV !== "dev" || !env.SEND_INVITATION_TEST) return normal;
17910
- const allowed2 = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
17911
- if (!allowed2.includes(recipientEmail.trim().toLowerCase())) return normal;
19073
+ const allowed = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
19074
+ if (!allowed.includes(recipientEmail.trim().toLowerCase())) return normal;
17912
19075
  const to = env.ODLA_INVITATION_TEST_TO?.trim();
17913
19076
  const from = env.ODLA_INVITATION_TEST_FROM?.trim();
17914
19077
  const valid = (value) => value && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(value);
@@ -17966,7 +19129,7 @@ function createMembershipChapterEffectRoute(options) {
17966
19129
  join.searchParams.set("seat", effect.seatId);
17967
19130
  join.searchParams.set("tier", effect.skuId);
17968
19131
  const template = config.emailTemplates.namedSeatInvitation;
17969
- if (template && !(0, import_crm26.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
19132
+ if (template && !(0, import_crm27.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
17970
19133
  config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: {
17971
19134
  ...template,
17972
19135
  text: "{{purchaserName}} has purchased a membership for you at {{chapterName}}.\n\n" + template.text
@@ -18428,27 +19591,27 @@ function validatedTokens(input) {
18428
19591
  }
18429
19592
  const tokens = {};
18430
19593
  let serializedLength = 0;
18431
- for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
18432
- if (!TOKEN_NAME.test(name)) {
18433
- throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
19594
+ for (const [name2, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
19595
+ if (!TOKEN_NAME.test(name2)) {
19596
+ throw new TypeError(`signup-control theme token "${name2}" must be an allowlisted --ui-* property`);
18434
19597
  }
18435
19598
  if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
18436
- throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
19599
+ throw new TypeError(`signup-control theme token "${name2}" must be a finite string or number`);
18437
19600
  }
18438
19601
  const value = String(raw2).trim();
18439
19602
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
18440
- throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
19603
+ throw new TypeError(`signup-control theme token "${name2}" has an unsafe or oversized value`);
18441
19604
  }
18442
- serializedLength += name.length + value.length + 4;
19605
+ serializedLength += name2.length + value.length + 4;
18443
19606
  if (serializedLength > MAX_SERIALIZED_LENGTH) {
18444
19607
  throw new TypeError("signup-control theme exceeds the serialized size bound");
18445
19608
  }
18446
- tokens[name] = value;
19609
+ tokens[name2] = value;
18447
19610
  }
18448
19611
  return tokens;
18449
19612
  }
18450
19613
  function cssFor(tokens) {
18451
- const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
19614
+ const declarations = Object.entries(tokens).map(([name2, value]) => ` ${name2}: ${value};`);
18452
19615
  return declarations.length ? `:root {
18453
19616
  ${declarations.join("\n")}
18454
19617
  }` : "";
@@ -18563,6 +19726,7 @@ var BUILTIN_ROUTES = [
18563
19726
  handleFormation,
18564
19727
  handleMember,
18565
19728
  handleMemberContent,
19729
+ handleMemberPortal,
18566
19730
  handleGifts,
18567
19731
  handleNamedSeat,
18568
19732
  handleNamedSeatRegift,