@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.
@@ -1,3 +1,17 @@
1
+ // src/admin-second-factor.ts
2
+ function sessionHasSecondFactor(payload) {
3
+ const fva = payload?.fva;
4
+ return Array.isArray(fva) && typeof fva[1] === "number" && fva[1] >= 0;
5
+ }
6
+ function adminSecondFactorAllows(mode, payload, who) {
7
+ if (mode === "off" || sessionHasSecondFactor(payload)) return true;
8
+ if (mode === "report") {
9
+ console.warn(JSON.stringify({ event: "chapter.admin_second_factor_missing", mode, who: who ?? null }));
10
+ return true;
11
+ }
12
+ return false;
13
+ }
14
+
1
15
  // src/worker-context.ts
2
16
  import { initAdmin } from "@odla-ai/db";
3
17
  import { createRemoteJWKSet, jwtVerify } from "jose";
@@ -27,9 +41,9 @@ function canChangeRole(ctx) {
27
41
  }
28
42
  return { ok: true };
29
43
  }
30
- async function getVaultSecret(db, name) {
44
+ async function getVaultSecret(db, name2) {
31
45
  try {
32
- const value = await db.secrets.get(name);
46
+ const value = await db.secrets.get(name2);
33
47
  return typeof value === "string" && value !== "" ? value : void 0;
34
48
  } catch {
35
49
  return void 0;
@@ -362,7 +376,8 @@ function createWorkerContext(options) {
362
376
  return await isAdminEmail(db, u.email) ? auth.adminRole : auth.ladder[0];
363
377
  }
364
378
  async function isAdmin(db, u) {
365
- return await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
379
+ const admin = await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
380
+ return admin && adminSecondFactorAllows(auth.adminSecondFactor ?? "off", u.payload, u.email ?? u.userId);
366
381
  }
367
382
  function crmSender(env) {
368
383
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return void 0;
@@ -850,14 +865,14 @@ function projectNetworkCommercialEvents(input, sources) {
850
865
  const prior = latest.get(key3);
851
866
  if (!prior || order(prior, fact) <= 0) latest.set(key3, row2);
852
867
  }
853
- const rows5 = [...latest.values()];
854
- const customers = rows5.filter((row2) => row2.objectKind === "customer");
855
- const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
856
- const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
857
- const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
858
- const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
859
- const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
860
- for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
868
+ const rows6 = [...latest.values()];
869
+ const customers = rows6.filter((row2) => row2.objectKind === "customer");
870
+ const subscriptions = rows6.filter((row2) => row2.objectKind === "subscription");
871
+ const invoices = rows6.filter((row2) => row2.objectKind === "invoice");
872
+ const seatPayments = rows6.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
873
+ const disputes = rows6.filter((row2) => row2.objectKind === "dispute");
874
+ const refunds = rows6.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
875
+ for (const charge of rows6.filter((row2) => row2.eventType === "charge.refunded")) {
861
876
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
862
877
  exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
863
878
  }
@@ -1115,11 +1130,11 @@ async function readStripeRecoveryEvent(db, eventId) {
1115
1130
  }
1116
1131
  async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
1117
1132
  const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
1118
- const rows5 = await db.query({
1133
+ const rows6 = await db.query({
1119
1134
  networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
1120
1135
  networkCommercialExceptions: { $: { where: { id: exceptionId2 }, limit: 1 } }
1121
1136
  });
1122
- const stored = rows5.networkCommercialEvents?.[0], exception = rows5.networkCommercialExceptions?.[0];
1137
+ const stored = rows6.networkCommercialEvents?.[0], exception = rows6.networkCommercialExceptions?.[0];
1123
1138
  if (!stored) throw new Error("stored event not found");
1124
1139
  const provider = await readStripeRecoveryEvent(db, eventId);
1125
1140
  const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
@@ -1195,7 +1210,7 @@ function findGiftRef(obj) {
1195
1210
  const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
1196
1211
  const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
1197
1212
  const candidates = [metaOf(obj), metaOf(subscription)];
1198
- const pickIdentity = (name) => candidates.map((metadata) => typeof metadata[name] === "string" ? String(metadata[name]) : void 0).find(Boolean);
1213
+ const pickIdentity = (name2) => candidates.map((metadata) => typeof metadata[name2] === "string" ? String(metadata[name2]) : void 0).find(Boolean);
1199
1214
  const appId = pickIdentity("odlaAppId");
1200
1215
  const environment = pickIdentity("odlaEnvironment");
1201
1216
  const dataEnvironment = pickIdentity("odlaDataEnvironment");
@@ -1292,11 +1307,11 @@ async function archivedChapterMembershipEvent(db, rawBody, app, env, ctx) {
1292
1307
  if (!app.membershipRestartId) return false;
1293
1308
  const fact = normalizeNetworkCommercialStripeEvent(JSON.parse(rawBody), "");
1294
1309
  if (!fact.subscriptionId || fact.subscriptionId === app.stripeSubscriptionId) return false;
1295
- const rows5 = (await db.query({ membershipCheckoutHistory: { $: { where: {
1310
+ const rows6 = (await db.query({ membershipCheckoutHistory: { $: { where: {
1296
1311
  applicationId: app.id,
1297
1312
  providerSubscriptionId: fact.subscriptionId
1298
1313
  }, limit: 2 } } })).membershipCheckoutHistory ?? [];
1299
- 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);
1314
+ 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);
1300
1315
  }
1301
1316
 
1302
1317
  // src/crm-sync.ts
@@ -1492,10 +1507,10 @@ async function findSharedRecord(deps, record4, tag) {
1492
1507
  if (found.crm_record?.[0]) return found.crm_record[0];
1493
1508
  }
1494
1509
  const nameField = def.nameField ?? "name";
1495
- const name = record4.input[nameField];
1496
- if (record4.type === "company" && typeof name === "string" && name.trim()) {
1510
+ const name2 = record4.input[nameField];
1511
+ if (record4.type === "company" && typeof name2 === "string" && name2.trim()) {
1497
1512
  const found = await deps.db.query({
1498
- crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
1513
+ crm_record: { $: { where: { type: record4.type, name: name2.trim() }, limit: 1 } }
1499
1514
  });
1500
1515
  if (found.crm_record?.[0]) return found.crm_record[0];
1501
1516
  }
@@ -1633,27 +1648,81 @@ async function backfillCrm(deps) {
1633
1648
  }
1634
1649
 
1635
1650
  // src/email-retirement.ts
1636
- function isRetiredEmailCopy(subject, text10) {
1651
+ function isRetiredEmailCopy(subject, text11) {
1637
1652
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1638
- const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1653
+ const bodyCopy = text11.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1639
1654
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1640
1655
  }
1641
1656
 
1642
1657
  // src/email.ts
1643
1658
  import { validateEmailCopy } from "@odla-ai/crm";
1644
1659
 
1660
+ // src/email-render.ts
1661
+ import { renderTemplate, templateLiveVars, templateSectionIssues, templateSectionNames, templateVars } from "@odla-ai/crm";
1662
+
1663
+ // src/email-html.ts
1664
+ var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
1665
+ function escapeHtml(s) {
1666
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1667
+ }
1668
+ function lineHtml(line) {
1669
+ let out = "", last = 0;
1670
+ for (const match of line.matchAll(URL_RE)) {
1671
+ out += escapeHtml(line.slice(last, match.index));
1672
+ const url = escapeHtml(match[0]);
1673
+ out += `<a href="${url}">${url}</a>`;
1674
+ last = match.index + match[0].length;
1675
+ }
1676
+ return out + escapeHtml(line.slice(last));
1677
+ }
1678
+ function textToHtml(text11) {
1679
+ const paragraphs = text11.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1680
+ const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
1681
+ return `<!doctype html>
1682
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
1683
+ <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">
1684
+ ${body}
1685
+ </body></html>`;
1686
+ }
1687
+
1688
+ // src/email-render.ts
1689
+ function render(template, vars) {
1690
+ return renderTemplate(template, vars);
1691
+ }
1692
+ var HTML_BODY_LIMIT = 2e5;
1693
+ function renderHtmlBody(template, vars) {
1694
+ const escaped = Object.fromEntries(Object.entries(vars).map(([name2, value]) => [name2, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
1695
+ return renderTemplate(template, escaped);
1696
+ }
1697
+ function htmlBodyIssues(html, options) {
1698
+ const issues = [];
1699
+ if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
1700
+ for (const message of templateSectionIssues(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
1701
+ const unknown = new Set(templateVars(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
1702
+ for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
1703
+ if (options.values) {
1704
+ for (const name2 of templateSectionNames(html)) if (typeof options.values[name2] !== "string") issues.push({ code: "value-missing", variable: name2, message: `Missing event value: ${name2} (HTML body).` });
1705
+ for (const variable of templateLiveVars(html, options.values)) {
1706
+ if (unknown.has(variable)) continue;
1707
+ const value = options.values[variable];
1708
+ if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
1709
+ }
1710
+ }
1711
+ return issues;
1712
+ }
1713
+
1645
1714
  // src/email-events.ts
1646
1715
  var identity = ["firstName", "lastName", "email", "phone", "state"];
1647
1716
  var policy = ["refundPolicyText", "commitmentText", "normsText"];
1648
- var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
1649
- var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
1717
+ var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
1718
+ var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
1650
1719
  var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
1651
1720
  var CHAPTER_EMAIL_EVENTS = {
1652
1721
  adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
1653
1722
  submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
1654
1723
  paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
1655
1724
  prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
1656
- onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
1725
+ onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
1657
1726
  namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
1658
1727
  seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
1659
1728
  };
@@ -1668,9 +1737,6 @@ function chapterEmailEvents(chapter) {
1668
1737
  }
1669
1738
 
1670
1739
  // src/email.ts
1671
- function render(template, vars) {
1672
- return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
1673
- }
1674
1740
  function groupVars(group, vars) {
1675
1741
  return {
1676
1742
  ...vars,
@@ -1703,7 +1769,9 @@ function planDelivery(input) {
1703
1769
  values: vars,
1704
1770
  tierId: vars.tierId
1705
1771
  });
1706
- if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
1772
+ const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
1773
+ const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
1774
+ if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
1707
1775
  const isProd = input.envName === "prod";
1708
1776
  const redirect = !isProd && !!input.group.debugEmail;
1709
1777
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
@@ -1713,35 +1781,12 @@ function planDelivery(input) {
1713
1781
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1714
1782
  if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1715
1783
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1716
- const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
1784
+ const text11 = redirect ? `(dev redirect; original recipient: ${input.to})
1717
1785
 
1718
1786
  ` + renderedText : renderedText;
1719
- return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
1720
- }
1721
-
1722
- // src/email-html.ts
1723
- var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
1724
- function escapeHtml(s) {
1725
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1726
- }
1727
- function lineHtml(line) {
1728
- let out = "", last = 0;
1729
- for (const match of line.matchAll(URL_RE)) {
1730
- out += escapeHtml(line.slice(last, match.index));
1731
- const url = escapeHtml(match[0]);
1732
- out += `<a href="${url}">${url}</a>`;
1733
- last = match.index + match[0].length;
1734
- }
1735
- return out + escapeHtml(line.slice(last));
1736
- }
1737
- function textToHtml(text10) {
1738
- const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1739
- const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
1740
- return `<!doctype html>
1741
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
1742
- <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">
1743
- ${body}
1744
- </body></html>`;
1787
+ const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
1788
+ 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;
1789
+ return { deliver: true, transport, to, subject, text: text11, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
1745
1790
  }
1746
1791
 
1747
1792
  // src/email-deliverability.ts
@@ -1777,9 +1822,9 @@ function tierPayable(tier, group, hasSecretKey) {
1777
1822
  function byOrder(a, b) {
1778
1823
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1779
1824
  }
1780
- function resolveTiers(group, rows5 = []) {
1781
- const stored = rows5.filter((tier) => tier.active);
1782
- if (rows5.length) return [...stored].sort(byOrder);
1825
+ function resolveTiers(group, rows6 = []) {
1826
+ const stored = rows6.filter((tier) => tier.active);
1827
+ if (rows6.length) return [...stored].sort(byOrder);
1783
1828
  return [{
1784
1829
  id: "standard",
1785
1830
  groupId: String(group.id ?? ""),
@@ -1835,16 +1880,16 @@ function rowToTier(row2, groupId) {
1835
1880
  }
1836
1881
  async function loadTiers(db, group) {
1837
1882
  const groupId = String(group.id ?? "");
1838
- let rows5;
1883
+ let rows6;
1839
1884
  try {
1840
- rows5 = (await db.query({
1885
+ rows6 = (await db.query({
1841
1886
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
1842
1887
  })).tiers;
1843
1888
  } catch {
1844
1889
  throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
1845
1890
  }
1846
- if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1847
- const list = rows5.map((row2) => rowToTier(row2, groupId));
1891
+ if (!Array.isArray(rows6)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1892
+ const list = rows6.map((row2) => rowToTier(row2, groupId));
1848
1893
  return resolveTiers(group, list);
1849
1894
  }
1850
1895
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -1873,10 +1918,14 @@ async function frozenCheckoutVariables(db, app) {
1873
1918
  if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
1874
1919
  formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
1875
1920
  }
1921
+ const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
1922
+ const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
1876
1923
  return {
1877
1924
  paymentAmount: formatted,
1878
1925
  paymentAmountSummary: formatted ? ` of ${formatted}` : "",
1879
- tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
1926
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
1927
+ namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
1928
+ namedSeatRecipientName
1880
1929
  };
1881
1930
  }
1882
1931
  async function catalogTierName(db, group, app) {
@@ -1905,8 +1954,8 @@ function formatInstant(at2, timeZone) {
1905
1954
  var adminUrlFor = (origin) => `${origin}/admin/`;
1906
1955
  var membersUrlFor = (origin) => `${origin}/members/`;
1907
1956
  function applicantVariables(app) {
1908
- const text10 = (v) => typeof v === "string" ? v : "";
1909
- return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
1957
+ const text11 = (v) => typeof v === "string" ? v : "";
1958
+ return { firstName: text11(app.firstName), lastName: text11(app.lastName), email: text11(app.email), phone: text11(app.phone), state: text11(app.state) };
1910
1959
  }
1911
1960
  function sampleVariables(origin, timeZone, now = Date.now()) {
1912
1961
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -1934,6 +1983,8 @@ function sampleVariables(origin, timeZone, now = Date.now()) {
1934
1983
  purchaserName: "Sample Purchaser",
1935
1984
  recipientName: "Sample Recipient",
1936
1985
  chapterName: "Sample Chapter",
1986
+ namedSeat: "paid",
1987
+ namedSeatRecipientName: "Sample Recipient",
1937
1988
  acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
1938
1989
  joinUrl: `${origin}/join?seat=sample`
1939
1990
  };
@@ -1950,8 +2001,8 @@ async function emailTierVariables(db, input) {
1950
2001
  if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
1951
2002
  const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
1952
2003
  if (!tierId) return { tierName: "", tier: "" };
1953
- const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
1954
- const tierName2 = isEmailTierName(name, tierId) ? name.trim() : "";
2004
+ const name2 = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
2005
+ const tierName2 = isEmailTierName(name2, tierId) ? name2.trim() : "";
1955
2006
  return { tierName: tierName2, tier: tierName2, tierId };
1956
2007
  } catch {
1957
2008
  return { tierName: "", tier: "" };
@@ -1959,8 +2010,8 @@ async function emailTierVariables(db, input) {
1959
2010
  }
1960
2011
 
1961
2012
  // src/notify.ts
1962
- function fromAddress(configured, name) {
1963
- const display = name?.trim();
2013
+ function fromAddress(configured, name2) {
2014
+ const display = name2?.trim();
1964
2015
  if (!display) return configured;
1965
2016
  const angled = configured.match(/<([^>]+)>/);
1966
2017
  return { name: display, email: (angled ? angled[1] : configured).trim() };
@@ -2031,14 +2082,14 @@ async function sendTemplated(deps, input) {
2031
2082
  if (decision.transport === "log-only") return { sent: true };
2032
2083
  let messageId;
2033
2084
  try {
2034
- const text10 = String(row2.body);
2035
- const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text10);
2085
+ const text11 = String(row2.body);
2086
+ const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text11);
2036
2087
  const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
2037
2088
  const response = await deps.sender.send({
2038
2089
  from: fromAddress(deps.from, input.group.fromName),
2039
2090
  to: [decision.to],
2040
2091
  subject: String(row2.subject),
2041
- text: text10,
2092
+ text: text11,
2042
2093
  ...html ? { html } : {},
2043
2094
  ...headers ? { headers } : {},
2044
2095
  replyTo: input.group.replyTo
@@ -2318,13 +2369,13 @@ async function authoritativeState(db, input) {
2318
2369
  return state;
2319
2370
  }
2320
2371
  async function currentPolicy(db, input, restartAware, state) {
2321
- const rows5 = await db.query({
2372
+ const rows6 = await db.query({
2322
2373
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
2323
2374
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
2324
2375
  });
2325
- if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2326
- 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));
2327
- 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);
2376
+ if (!Array.isArray(rows6.membershipOfferingVersion) || !Array.isArray(rows6.discountPolicyVersion) || rows6.membershipOfferingVersion.length >= 200 || rows6.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2377
+ 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));
2378
+ 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);
2328
2379
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
2329
2380
  const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
2330
2381
  const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
@@ -2442,13 +2493,13 @@ async function membershipRestartPayment(db, target, member, now) {
2442
2493
  // src/checkout-named-seat.ts
2443
2494
  function checkoutNamedSeatSelection(value, purchaserEmail) {
2444
2495
  const row2 = value;
2445
- const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
2496
+ const name2 = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
2446
2497
  const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
2447
2498
  const purchaser = normalizeChapterEmail(purchaserEmail);
2448
- 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) {
2499
+ 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) {
2449
2500
  throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
2450
2501
  }
2451
- return { recipientName: name, recipientEmail: email };
2502
+ return { recipientName: name2, recipientEmail: email };
2452
2503
  }
2453
2504
  async function checkoutNamedSeatIdentity(target, email) {
2454
2505
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -2889,14 +2940,14 @@ async function readMembershipAccess(db, input, now) {
2889
2940
  const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
2890
2941
  requireMembershipScope(entitlement, input);
2891
2942
  const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
2892
- 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;
2943
+ 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;
2893
2944
  return {
2894
2945
  membershipId: id7,
2895
- allowed: allowed2,
2946
+ allowed,
2896
2947
  status: String(member.status),
2897
2948
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
2898
2949
  revision: Number(member.revision),
2899
- restartEligible: !allowed2 && membershipCanRestart(member, now)
2950
+ restartEligible: !allowed && membershipCanRestart(member, now)
2900
2951
  };
2901
2952
  }
2902
2953
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -3041,11 +3092,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
3041
3092
  if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
3042
3093
  throw new Error("Stripe returned an invalid commercial list response");
3043
3094
  }
3044
- const rows5 = page.data.map(asRaw).filter((row2) => row2 !== null);
3045
- if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3046
- objects.push(...rows5);
3095
+ const rows6 = page.data.map(asRaw).filter((row2) => row2 !== null);
3096
+ if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3097
+ objects.push(...rows6);
3047
3098
  if (!page.has_more) return { objects, complete: true };
3048
- const cursor = rows5.at(-1)?.id;
3099
+ const cursor = rows6.at(-1)?.id;
3049
3100
  if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
3050
3101
  startingAfter = cursor;
3051
3102
  }
@@ -3178,7 +3229,7 @@ async function approveApplication(input) {
3178
3229
  try {
3179
3230
  if (!duplicate) {
3180
3231
  const write = await db.transact(
3181
- [{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
3232
+ [{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvedAt: Date.now(), approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
3182
3233
  { mutationId: `approve:${id7}`, guards: [{
3183
3234
  ns: "applications",
3184
3235
  id: id7,
@@ -3461,8 +3512,8 @@ async function issueGiftClaim(db, intent, provider, now) {
3461
3512
  throw new Error("gift issuance conflict");
3462
3513
  }
3463
3514
  }
3464
- function buyerGiftClaims(rows5, now) {
3465
- return rows5.map((row2) => {
3515
+ function buyerGiftClaims(rows6, now) {
3516
+ return rows6.map((row2) => {
3466
3517
  const stored = String(row2.status ?? "pending");
3467
3518
  const status2 = stored === "pending" && Number(row2.codeExpiresAt) <= now ? "expired" : stored;
3468
3519
  return {
@@ -3608,8 +3659,8 @@ async function revokeGiftClaim(db, claim, reason, now) {
3608
3659
  // src/payments-route-support.ts
3609
3660
  async function findPaymentApplication(db, event2) {
3610
3661
  if ("applicationId" in event2 && event2.applicationId) {
3611
- const rows5 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3612
- return rows5.applications?.[0];
3662
+ const rows6 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3663
+ return rows6.applications?.[0];
3613
3664
  }
3614
3665
  if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
3615
3666
  const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
@@ -3800,6 +3851,306 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3800
3851
  return membershipFail("membership_dispute_retry_required");
3801
3852
  }
3802
3853
 
3854
+ // src/member-system-authority.ts
3855
+ var rows2 = (group) => group.fields.map((id7) => ({ ...group, id: id7 }));
3856
+ var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
3857
+ ...rows2({
3858
+ scope: "network",
3859
+ writer: "network_person",
3860
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3861
+ sensitivity: "pii",
3862
+ memberAccess: "edit",
3863
+ fields: [
3864
+ "person.legalName",
3865
+ "person.preferredName",
3866
+ "person.namePronunciation",
3867
+ "person.pronouns",
3868
+ "person.displayLegalName",
3869
+ "person.primaryEmail",
3870
+ "person.secondaryEmails",
3871
+ "person.phone",
3872
+ "person.linkedinUrl",
3873
+ "person.photo",
3874
+ "person.homeAddress",
3875
+ "person.fulfillmentFit",
3876
+ "person.fulfillmentSize"
3877
+ ]
3878
+ }),
3879
+ ...rows2({
3880
+ scope: "network",
3881
+ writer: "network_person",
3882
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3883
+ sensitivity: "ordinary",
3884
+ memberAccess: "edit",
3885
+ fields: [
3886
+ "person.title",
3887
+ "person.company",
3888
+ "person.companyObservedAt",
3889
+ "person.region",
3890
+ "person.whyText",
3891
+ "person.whyTokens",
3892
+ "person.skillsOffered",
3893
+ "person.openToRequests",
3894
+ "person.directoryPhotoConsent",
3895
+ "person.profilePrompts",
3896
+ "person.introducedByText"
3897
+ ]
3898
+ }),
3899
+ ...rows2({
3900
+ scope: "network",
3901
+ writer: "network_person",
3902
+ projections: ["bnf_admin", "chapter_admin"],
3903
+ sensitivity: "restricted",
3904
+ memberAccess: "none",
3905
+ fields: [
3906
+ "person.linkedinMemberId",
3907
+ "person.identityClaims",
3908
+ "person.aliases",
3909
+ "person.mergeHistory",
3910
+ "person.introducedByPersonId",
3911
+ "person.introducedAt",
3912
+ "person.firstTouch",
3913
+ "person.globalSuppression",
3914
+ "person.deceasedAt",
3915
+ "person.archivedAt"
3916
+ ]
3917
+ }),
3918
+ ...rows2({
3919
+ scope: "network",
3920
+ writer: "network_person",
3921
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3922
+ sensitivity: "sensitive",
3923
+ memberAccess: "edit",
3924
+ fields: ["person.accreditationSelfCertification", "person.accreditationCategory", "person.accreditationExpiresAt"]
3925
+ }),
3926
+ ...rows2({
3927
+ scope: "network",
3928
+ writer: "network_person",
3929
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3930
+ sensitivity: "restricted",
3931
+ memberAccess: "read",
3932
+ fields: ["person.acknowledgements"]
3933
+ }),
3934
+ ...rows2({
3935
+ scope: "provider",
3936
+ writer: "clerk",
3937
+ projections: ["clerk_identity", "bnf_admin", "chapter_admin"],
3938
+ sensitivity: "restricted",
3939
+ memberAccess: "none",
3940
+ fields: ["identity.clerkUserIds", "identity.clerkEmailVerification"]
3941
+ }),
3942
+ ...rows2({
3943
+ scope: "chapter",
3944
+ writer: "chapter_journey",
3945
+ projections: ["chapter_admin", "chapter_crm", "bnf_admin"],
3946
+ sensitivity: "ordinary",
3947
+ memberAccess: "none",
3948
+ fields: [
3949
+ "journey.chapterId",
3950
+ "journey.stage",
3951
+ "journey.stageEnteredAt",
3952
+ "journey.stageHistory",
3953
+ "journey.ending",
3954
+ "journey.endingReason",
3955
+ "journey.source",
3956
+ "journey.entryChapterId",
3957
+ "journey.route",
3958
+ "journey.flags",
3959
+ "journey.applicationStartedAt",
3960
+ "journey.appliedAt",
3961
+ "journey.paidAt",
3962
+ "journey.callBookedAt",
3963
+ "journey.metAt",
3964
+ "journey.approvedAt",
3965
+ "journey.onboardedAt",
3966
+ "journey.firstLoginAt",
3967
+ "journey.firstDealAt"
3968
+ ]
3969
+ }),
3970
+ ...rows2({
3971
+ scope: "chapter",
3972
+ writer: "chapter_journey",
3973
+ projections: ["chapter_admin", "chapter_crm"],
3974
+ sensitivity: "sensitive",
3975
+ memberAccess: "none",
3976
+ fields: [
3977
+ "journey.relationshipOwnerId",
3978
+ "journey.lastContactAt",
3979
+ "journey.nextAction",
3980
+ "journey.owes",
3981
+ "journey.asks",
3982
+ "journey.preparation",
3983
+ "journey.notes"
3984
+ ]
3985
+ }),
3986
+ ...rows2({
3987
+ scope: "chapter",
3988
+ writer: "chapter_journey",
3989
+ projections: ["chapter_admin", "chapter_crm", "member_self_service"],
3990
+ sensitivity: "ordinary",
3991
+ memberAccess: "edit",
3992
+ fields: ["journey.dealFlowConsent", "journey.directoryConsent", "journey.emailConsent", "journey.smsConsent"]
3993
+ }),
3994
+ ...rows2({
3995
+ scope: "chapter",
3996
+ writer: "chapter_journey",
3997
+ projections: ["chapter_admin", "bnf_admin", "member_self_service"],
3998
+ sensitivity: "ordinary",
3999
+ memberAccess: "read",
4000
+ fields: ["journey.memberNumber"]
4001
+ }),
4002
+ ...rows2({
4003
+ scope: "network",
4004
+ writer: "network_membership",
4005
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4006
+ sensitivity: "ordinary",
4007
+ memberAccess: "read",
4008
+ fields: [
4009
+ "membership.chapterId",
4010
+ "membership.journeyId",
4011
+ "membership.termStartsAt",
4012
+ "membership.termEndsAt",
4013
+ "membership.tier",
4014
+ "membership.status",
4015
+ "membership.joinedAt",
4016
+ "membership.autoRenews",
4017
+ "membership.renewalAt",
4018
+ "membership.cancelsAt",
4019
+ "membership.graceEndsAt",
4020
+ "membership.foundingCohort",
4021
+ "membership.offeringPolicyVersion",
4022
+ "membership.discountPolicyVersion"
4023
+ ]
4024
+ }),
4025
+ ...rows2({
4026
+ scope: "network",
4027
+ writer: "network_commercial_projection",
4028
+ projections: ["bnf_admin", "chapter_admin"],
4029
+ sensitivity: "restricted",
4030
+ memberAccess: "none",
4031
+ fields: [
4032
+ "membership.stripeCustomerId",
4033
+ "membership.stripeSubscriptionId",
4034
+ "membership.stripePriceId",
4035
+ "membership.lastInvoiceId",
4036
+ "membership.paymentOutcome",
4037
+ "membership.refundOutcome"
4038
+ ]
4039
+ }),
4040
+ ...rows2({
4041
+ scope: "network",
4042
+ writer: "network_discount_policy",
4043
+ projections: ["bnf_admin", "chapter_admin", "stripe_billing"],
4044
+ sensitivity: "ordinary",
4045
+ memberAccess: "none",
4046
+ fields: [
4047
+ "discountPolicy.chapterId",
4048
+ "discountPolicy.version",
4049
+ "discountPolicy.eligibleOfferingIds",
4050
+ "discountPolicy.percentOff",
4051
+ "discountPolicy.amountOff",
4052
+ "discountPolicy.activatesOn",
4053
+ "discountPolicy.invoiceCount",
4054
+ "discountPolicy.expiresAt",
4055
+ "discountPolicy.stackingOrder",
4056
+ "discountPolicy.status"
4057
+ ]
4058
+ }),
4059
+ ...rows2({
4060
+ scope: "network",
4061
+ writer: "network_entitlement",
4062
+ projections: ["bnf_admin", "chapter_admin", "member_self_service", "stripe_billing"],
4063
+ sensitivity: "ordinary",
4064
+ memberAccess: "read",
4065
+ fields: [
4066
+ "entitlement.membershipId",
4067
+ "entitlement.kind",
4068
+ "entitlement.priceCents",
4069
+ "entitlement.currency",
4070
+ "entitlement.payerPersonId",
4071
+ "entitlement.purchasedAt",
4072
+ "entitlement.termEndsAt",
4073
+ "entitlement.renewsWithMembership",
4074
+ "entitlement.status"
4075
+ ]
4076
+ }),
4077
+ ...rows2({
4078
+ scope: "network",
4079
+ writer: "network_seat",
4080
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4081
+ sensitivity: "pii",
4082
+ memberAccess: "read",
4083
+ fields: [
4084
+ "seat.membershipId",
4085
+ "seat.entitlementId",
4086
+ "seat.personId",
4087
+ "seat.recipientName",
4088
+ "seat.recipientEmail",
4089
+ "seat.role",
4090
+ "seat.status",
4091
+ "seat.acceptanceExpiresAt",
4092
+ "seat.acceptedAt",
4093
+ "seat.activatedAt",
4094
+ "seat.refundedAt"
4095
+ ]
4096
+ }),
4097
+ ...rows2({
4098
+ scope: "network",
4099
+ writer: "network_referral_ledger",
4100
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4101
+ sensitivity: "ordinary",
4102
+ memberAccess: "read",
4103
+ fields: [
4104
+ "referral.referrerPersonId",
4105
+ "referral.referredPersonId",
4106
+ "referral.qualifiedMembershipId",
4107
+ "referral.creditCents",
4108
+ "referral.status",
4109
+ "referral.appliedInvoiceId",
4110
+ "referral.carryForwardCents",
4111
+ "referral.adjustmentOf"
4112
+ ]
4113
+ }),
4114
+ ...rows2({
4115
+ scope: "network",
4116
+ writer: "network_investment_ledger",
4117
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4118
+ sensitivity: "restricted",
4119
+ memberAccess: "read",
4120
+ fields: [
4121
+ "investment.personId",
4122
+ "investment.chapterId",
4123
+ "investment.season",
4124
+ "investment.purpose",
4125
+ "investment.targetCount",
4126
+ "investment.status",
4127
+ "investment.amountCents",
4128
+ "investment.closedAt",
4129
+ "investment.sourceReference"
4130
+ ]
4131
+ }),
4132
+ ...rows2({
4133
+ scope: "provider",
4134
+ writer: "stripe",
4135
+ projections: ["stripe_billing", "bnf_admin", "chapter_admin"],
4136
+ sensitivity: "restricted",
4137
+ memberAccess: "none",
4138
+ fields: [
4139
+ "commercial.charge",
4140
+ "commercial.invoice",
4141
+ "commercial.refund",
4142
+ "commercial.providerSubscription",
4143
+ "commercial.paymentMethod"
4144
+ ]
4145
+ })
4146
+ ];
4147
+ var MEMBER_SELF_EDITABLE_FIELDS = new Set(
4148
+ MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess === "edit").map((row2) => row2.id)
4149
+ );
4150
+ var MEMBER_SELF_READABLE_FIELDS = new Set(
4151
+ MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
4152
+ );
4153
+
3803
4154
  // src/member-system-contract.ts
3804
4155
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3805
4156
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3811,8 +4162,8 @@ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink)
3811
4162
 
3812
4163
  // src/stripe-webhook-partition.ts
3813
4164
  var firstRow = async (db, ns, q) => {
3814
- const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
3815
- return Array.isArray(rows5) ? rows5[0] : void 0;
4165
+ const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
4166
+ return Array.isArray(rows6) ? rows6[0] : void 0;
3816
4167
  };
3817
4168
  var stripeEventIdentity = (event2) => ({
3818
4169
  ...event2.appId ? { appId: event2.appId } : {},
@@ -3919,8 +4270,8 @@ async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest,
3919
4270
  // src/stripe-event-reconcile.ts
3920
4271
  async function firstRow2(db, ns, q) {
3921
4272
  const res = await db.query({ [ns]: { $: q } });
3922
- const rows5 = res[ns];
3923
- return Array.isArray(rows5) ? rows5[0] : void 0;
4273
+ const rows6 = res[ns];
4274
+ return Array.isArray(rows6) ? rows6[0] : void 0;
3924
4275
  }
3925
4276
  async function reconcileChapterStripeEvent(input, env, ctx) {
3926
4277
  const db = ctx.makeDb(env);
@@ -4237,19 +4588,19 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
4237
4588
  const db = raw2, target = stripeEventTarget(env, ctx);
4238
4589
  const local4 = (row3) => row3.chapterId === target.chapterId && row3.appId === target.appId && row3.environment === target.environment && row3.runtime === target.runtime;
4239
4590
  if (req.method === "GET") {
4240
- const rows5 = (await db.query({ stripeEventReceipts: { $: {
4591
+ const rows6 = (await db.query({ stripeEventReceipts: { $: {
4241
4592
  where: { chapterId: target.chapterId, runtime: target.runtime },
4242
4593
  order: { receivedAt: "desc" },
4243
4594
  limit: 500
4244
4595
  } } })).stripeEventReceipts ?? [];
4245
- const pending = rows5.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
4596
+ const pending = rows6.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
4246
4597
  return json({ rows: pending.map((row3) => ({
4247
4598
  eventId: row3.eventId,
4248
4599
  kind: row3.kind,
4249
4600
  disposition: row3.disposition,
4250
4601
  receivedAt: row3.receivedAt,
4251
4602
  retryReady: row3.retryReady === true
4252
- })), truncated: rows5.length >= 500 });
4603
+ })), truncated: rows6.length >= 500 });
4253
4604
  }
4254
4605
  let eventId;
4255
4606
  try {
@@ -4435,9 +4786,9 @@ function memberApplication(app, meeting, defaultTimezone) {
4435
4786
  var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
4436
4787
  async function memberSessionApplication(db, chapterId, runtime, identity2) {
4437
4788
  const lookup = async (where) => {
4438
- const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4439
- if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
4440
- return rows5.filter((row2) => local(row2, chapterId, runtime));
4789
+ const rows6 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4790
+ if (!Array.isArray(rows6) || rows6.length >= 100) throw new Error("member identity lookup incomplete");
4791
+ return rows6.filter((row2) => local(row2, chapterId, runtime));
4441
4792
  };
4442
4793
  let apps = await lookup({ clerkUserId: identity2.userId });
4443
4794
  let emailFallback = false;
@@ -4814,10 +5165,10 @@ async function namedSeatRecipientClaimId(input, email) {
4814
5165
  }
4815
5166
  async function reserveNamedMembershipSeat(db, input, now) {
4816
5167
  membershipClock(now);
4817
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
5168
+ const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4818
5169
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4819
5170
  const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4820
- 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");
5171
+ 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");
4821
5172
  const state = await loadMembershipAuthorityState(db, input);
4822
5173
  const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4823
5174
  requireMembershipScope(member, input);
@@ -4834,7 +5185,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4834
5185
  id: id7,
4835
5186
  ...membershipScope(input),
4836
5187
  primaryMembershipId: memberId,
4837
- recipientName: name,
5188
+ recipientName: name2,
4838
5189
  recipientEmailNormalized: email,
4839
5190
  purchaserUserId: input.purchaserUserId,
4840
5191
  recipientClaimId: claimId,
@@ -4881,7 +5232,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4881
5232
  if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4882
5233
  }
4883
5234
  requireMembershipScope(seat, input);
4884
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
5235
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name2 || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4885
5236
  return seat;
4886
5237
  }
4887
5238
 
@@ -5072,9 +5423,9 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5072
5423
  membershipClock(now);
5073
5424
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
5074
5425
  const state = await loadMembershipAuthorityState(db, scope2);
5075
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5426
+ const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5076
5427
  let lifted = 0;
5077
- for (const member of rows5.slice(0, limit)) {
5428
+ for (const member of rows6.slice(0, limit)) {
5078
5429
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
5079
5430
  const offering = membershipOfferingVersion(member.offeringSnapshot);
5080
5431
  if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
@@ -5090,7 +5441,7 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5090
5441
  ] });
5091
5442
  lifted++;
5092
5443
  }
5093
- return { lifted, more: rows5.length > limit };
5444
+ return { lifted, more: rows6.length > limit };
5094
5445
  }
5095
5446
 
5096
5447
  // src/membership-seat-reprice.ts
@@ -5103,9 +5454,9 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
5103
5454
  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];
5104
5455
  if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
5105
5456
  const target = current.seatPriceCents;
5106
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5107
- const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
5108
- for (const member of rows5.slice(0, limit)) {
5457
+ const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5458
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows6.length > limit };
5459
+ for (const member of rows6.slice(0, limit)) {
5109
5460
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
5110
5461
  const id7 = String(member.id), skip = (reason) => {
5111
5462
  receipt.skipped.push({ membershipId: id7, reason });
@@ -5204,8 +5555,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
5204
5555
  }
5205
5556
  }
5206
5557
  if (!one) {
5207
- const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5208
- return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
5558
+ const rows6 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5559
+ return json({ seats: rows6.slice(0, 200).map(project), truncated: rows6.length > 200 });
5209
5560
  }
5210
5561
  const seat = await membershipLoad(db, "networkSeat", one[1]);
5211
5562
  if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
@@ -5424,8 +5775,8 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5424
5775
  const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
5425
5776
  const found = await db.query({ networkMembership: page, networkSeat: page });
5426
5777
  const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
5427
- const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5428
- const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5778
+ const rows6 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5779
+ const disputes = rows6.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5429
5780
  return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
5430
5781
  }
5431
5782
  const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
@@ -5475,9 +5826,9 @@ function stateOf(item, all, now, window, rivals) {
5475
5826
  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";
5476
5827
  var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
5477
5828
  var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
5478
- function normalize(rows5, collection, parse, invalid2) {
5829
+ function normalize(rows6, collection, parse, invalid2) {
5479
5830
  const out = [];
5480
- for (const row2 of rows5) {
5831
+ for (const row2 of rows6) {
5481
5832
  try {
5482
5833
  out.push({ value: parse(row2.snapshot), row: row2 });
5483
5834
  } catch (error) {
@@ -5488,11 +5839,11 @@ function normalize(rows5, collection, parse, invalid2) {
5488
5839
  }
5489
5840
  async function listMembershipPolicyVersions(db, input) {
5490
5841
  const where = input.chapterId ? { chapterId: input.chapterId } : {};
5491
- const rows5 = await db.query({
5842
+ const rows6 = await db.query({
5492
5843
  membershipOfferingVersion: { $: { where, limit: LIMIT } },
5493
5844
  discountPolicyVersion: { $: { where, limit: LIMIT } }
5494
5845
  });
5495
- const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
5846
+ const offeringRows = rows6.membershipOfferingVersion ?? [], policyRows = rows6.discountPolicyVersion ?? [];
5496
5847
  const invalid2 = [];
5497
5848
  const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
5498
5849
  const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
@@ -5632,22 +5983,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
5632
5983
  const skip = (membershipId, reason) => {
5633
5984
  receipt.skipped.push({ membershipId, reason });
5634
5985
  };
5635
- let rows5;
5986
+ let rows6;
5636
5987
  if (input.membershipIds) {
5637
- rows5 = [];
5988
+ rows6 = [];
5638
5989
  for (const id7 of input.membershipIds) {
5639
5990
  const member = await membershipLoad(db, "networkMembership", id7);
5640
5991
  if (!member) skip(id7, "not_found");
5641
5992
  else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
5642
- else rows5.push(member);
5993
+ else rows6.push(member);
5643
5994
  }
5644
5995
  } else {
5645
5996
  const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5646
5997
  if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
5647
5998
  receipt.more = found.length > limit;
5648
- rows5 = found.slice(0, limit);
5999
+ rows6 = found.slice(0, limit);
5649
6000
  }
5650
- for (const member of rows5) {
6001
+ for (const member of rows6) {
5651
6002
  const id7 = String(member.id);
5652
6003
  if (member.discountPolicyVersionId === policy2.id) {
5653
6004
  receipt.unchanged.push(id7);
@@ -5943,13 +6294,13 @@ var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
5943
6294
  function resolveApplication(a) {
5944
6295
  const required = a?.required ?? DEFAULT_REQUIRED;
5945
6296
  const optional = a?.optional ?? DEFAULT_OPTIONAL;
5946
- for (const [name, arr] of [["required", required], ["optional", optional]]) {
6297
+ for (const [name2, arr] of [["required", required], ["optional", optional]]) {
5947
6298
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
5948
- throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
6299
+ throw new Error(`defineChapter.application.${name2}: must be an array of field-name strings`);
5949
6300
  }
5950
6301
  const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
5951
6302
  if (unsafe) {
5952
- throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
6303
+ throw new Error(`defineChapter.application.${name2}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
5953
6304
  }
5954
6305
  }
5955
6306
  const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
@@ -6001,14 +6352,14 @@ function hasDisclaimerAck(fields) {
6001
6352
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
6002
6353
  function applicantProfile(chapter, fields) {
6003
6354
  const app = chapter.application;
6004
- const allowed2 = (f) => app.profileFields.includes(f);
6355
+ const allowed = (f) => app.profileFields.includes(f);
6005
6356
  const profile = {};
6006
6357
  for (const f of [...app.required, ...app.optional]) {
6007
- if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
6358
+ if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
6008
6359
  const v = fields[f];
6009
6360
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
6010
6361
  }
6011
- if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
6362
+ if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
6012
6363
  return Object.keys(profile).length > 0 ? profile : void 0;
6013
6364
  }
6014
6365
  async function submitApplication(db, chapter, fields, opts) {
@@ -6127,10 +6478,10 @@ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
6127
6478
  applications: { $: { where: { emailNormalized: email }, limit: 10 } },
6128
6479
  $users: { $: { where: { email }, limit: 2 } }
6129
6480
  });
6130
- const rows5 = data.applications ?? [], others = rows5.filter((row2) => row2.id !== applicationId);
6481
+ const rows6 = data.applications ?? [], others = rows6.filter((row2) => row2.id !== applicationId);
6131
6482
  const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
6132
6483
  if (others.length !== closed.length) return conflict();
6133
- const original = rows5.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
6484
+ const original = rows6.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
6134
6485
  const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
6135
6486
  if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
6136
6487
  const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
@@ -6254,15 +6605,15 @@ async function signupControlDigest(document) {
6254
6605
 
6255
6606
  // src/admission-store-read.ts
6256
6607
  async function readAdmissionRows(db, namespace, where, maximum) {
6257
- const rows5 = [];
6608
+ const rows6 = [];
6258
6609
  for (let offset = 0; offset <= maximum; offset += 500) {
6259
6610
  const result = await db.query({ [namespace]: { $: { where, order: { id: "asc" }, limit: 500, offset } } });
6260
6611
  const page = Array.isArray(result[namespace]) ? result[namespace] : [];
6261
- rows5.push(...page);
6262
- if (rows5.length > maximum) throw new Error("admission_assignment_limit");
6612
+ rows6.push(...page);
6613
+ if (rows6.length > maximum) throw new Error("admission_assignment_limit");
6263
6614
  if (page.length < 500) break;
6264
6615
  }
6265
- return rows5;
6616
+ return rows6;
6266
6617
  }
6267
6618
 
6268
6619
  // src/admission-grants.ts
@@ -6345,7 +6696,7 @@ async function createAdmissionGrantEnvelope(document) {
6345
6696
  if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
6346
6697
  return { document: checked.value, digest: await signupControlDigest(checked.value) };
6347
6698
  }
6348
- var rows2 = (value) => Array.isArray(value) ? value : [];
6699
+ var rows3 = (value) => Array.isArray(value) ? value : [];
6349
6700
  function asGrant(row2) {
6350
6701
  if (typeof row2.id !== "string" || typeof row2.emailNormalized !== "string") return null;
6351
6702
  if (row2.sourceScope !== "local" && row2.sourceScope !== "parent") return null;
@@ -6356,7 +6707,7 @@ async function findAdmissionGrant(db, targetId, email, now) {
6356
6707
  const emailNormalized = normalizeAdmissionEmail(email);
6357
6708
  if (!isAdmissionEmail(emailNormalized)) return null;
6358
6709
  const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
6359
- const active = rows2(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6710
+ const active = rows3(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6360
6711
  grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
6361
6712
  )).sort((a, b) => {
6362
6713
  const scope2 = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
@@ -6370,7 +6721,7 @@ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
6370
6721
  return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
6371
6722
  }
6372
6723
  const found = await db.query({ admissionGrantRevisions: {} });
6373
- return rows2(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
6724
+ return rows3(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
6374
6725
  }
6375
6726
  async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
6376
6727
  if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
@@ -6379,7 +6730,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
6379
6730
  order: { revision: "desc" },
6380
6731
  limit: 1
6381
6732
  } } });
6382
- const row2 = rows2(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
6733
+ const row2 = rows3(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
6383
6734
  return row2 ? row2 : null;
6384
6735
  }
6385
6736
  async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
@@ -6458,8 +6809,8 @@ var APPLICATION_LABELS = {
6458
6809
  state: "State"
6459
6810
  };
6460
6811
  function humanize(id7) {
6461
- const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6462
- return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
6812
+ const text11 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6813
+ return text11 ? `${text11[0].toUpperCase()}${text11.slice(1)}` : id7;
6463
6814
  }
6464
6815
  function applicationDefs(chapter) {
6465
6816
  try {
@@ -6552,8 +6903,8 @@ function validateGiftOfferCatalog(input, tierIds) {
6552
6903
  errors.push(`${path} must be an object`);
6553
6904
  continue;
6554
6905
  }
6555
- const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6556
- for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6906
+ const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6907
+ for (const key3 of Object.keys(item)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
6557
6908
  const id7 = text5(item.id, `${path}.id`, errors, 80);
6558
6909
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6559
6910
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
@@ -6617,27 +6968,27 @@ async function hasSignupControl(db, targetId, environment, active) {
6617
6968
  if (db.aggregate) {
6618
6969
  return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
6619
6970
  }
6620
- const rows5 = (await db.query({ signupControls: {} })).signupControls;
6621
- return Array.isArray(rows5) && rows5.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
6971
+ const rows6 = (await db.query({ signupControls: {} })).signupControls;
6972
+ return Array.isArray(rows6) && rows6.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
6622
6973
  }
6623
6974
  async function loadLatestSignupControl(db, targetId, environment) {
6624
6975
  if (!await hasSignupControl(db, targetId, environment)) return null;
6625
- const rows5 = (await db.query({ signupControls: { $: {
6976
+ const rows6 = (await db.query({ signupControls: { $: {
6626
6977
  where: { targetId, environment },
6627
6978
  order: { revision: "desc" },
6628
6979
  limit: 1
6629
6980
  } } })).signupControls;
6630
- const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6981
+ const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6631
6982
  return asStored(latest);
6632
6983
  }
6633
6984
  async function loadActiveSignupControl(db, targetId, environment) {
6634
6985
  if (!await hasSignupControl(db, targetId, environment, true)) return null;
6635
- const rows5 = (await db.query({ signupControls: { $: {
6986
+ const rows6 = (await db.query({ signupControls: { $: {
6636
6987
  where: { targetId, environment, active: true },
6637
6988
  order: { revision: "desc" },
6638
6989
  limit: 1
6639
6990
  } } })).signupControls;
6640
- const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6991
+ const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6641
6992
  return asStored(active);
6642
6993
  }
6643
6994
  function controlledApplication(base, document) {
@@ -7010,17 +7361,17 @@ async function readBoundedRequestText(req, maxBytes) {
7010
7361
  if (!reader) return "";
7011
7362
  const decoder = new TextDecoder();
7012
7363
  let bytes = 0;
7013
- let text10 = "";
7364
+ let text11 = "";
7014
7365
  for (; ; ) {
7015
7366
  const { done, value } = await reader.read();
7016
7367
  if (req.signal.aborted) return invalid2();
7017
7368
  if (done) {
7018
7369
  complete = true;
7019
- return text10 + decoder.decode();
7370
+ return text11 + decoder.decode();
7020
7371
  }
7021
7372
  bytes += value.byteLength;
7022
7373
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
7023
- text10 += decoder.decode(value, { stream: true });
7374
+ text11 += decoder.decode(value, { stream: true });
7024
7375
  }
7025
7376
  } catch {
7026
7377
  return invalid2();
@@ -7124,9 +7475,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
7124
7475
  let selection;
7125
7476
  if (request?.body) {
7126
7477
  try {
7127
- const text10 = await readBoundedRequestText(request, 2048);
7128
- if (text10 instanceof Response) return text10;
7129
- const body = JSON.parse(text10);
7478
+ const text11 = await readBoundedRequestText(request, 2048);
7479
+ if (text11 instanceof Response) return text11;
7480
+ const body = JSON.parse(text11);
7130
7481
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
7131
7482
  selection = body.namedSeat;
7132
7483
  } catch {
@@ -7279,9 +7630,9 @@ async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
7279
7630
  if (!app || app.groupId !== chapterId || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(runtime)) {
7280
7631
  throw new Error("invitation purchaser unavailable");
7281
7632
  }
7282
- const name = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7283
- if (!name) throw new Error("invitation purchaser name unavailable");
7284
- return name.slice(0, 160);
7633
+ const name2 = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7634
+ if (!name2) throw new Error("invitation purchaser name unavailable");
7635
+ return name2.slice(0, 160);
7285
7636
  }
7286
7637
  async function namedSeatApplicationContext(req, seatId, env, ctx) {
7287
7638
  const seats = ctx.membershipAuthority?.namedSeats;
@@ -7310,10 +7661,10 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
7310
7661
  }
7311
7662
  async function existingNamedSeatApplication(context, env, ctx) {
7312
7663
  const { db, userId, email, invitation } = context;
7313
- const rows5 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7314
- const apps = rows5.filter((row2) => !closedApplication(row2));
7664
+ const rows6 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7665
+ const apps = rows6.filter((row2) => !closedApplication(row2));
7315
7666
  if (!apps.length) {
7316
- const closed = rows5.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
7667
+ const closed = rows6.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
7317
7668
  return { predecessor: closed ? { id: String(closed.id) } : null };
7318
7669
  }
7319
7670
  const app = apps[0];
@@ -7678,6 +8029,111 @@ async function notifySubmitConfirmed(db, env, notice) {
7678
8029
  }
7679
8030
  }
7680
8031
 
8032
+ // src/member-entitlement.ts
8033
+ function entitledTierId(app, now = Date.now()) {
8034
+ const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
8035
+ const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
8036
+ if (granted && (endsAt === null || endsAt > now)) return granted;
8037
+ return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
8038
+ }
8039
+
8040
+ // src/portal-member.ts
8041
+ var text6 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8042
+ async function activeHubGrant(db, identity2) {
8043
+ const byUser = (await db.query({ hubMemberGrants: { $: { where: { userId: identity2.userId }, limit: 5 } } })).hubMemberGrants ?? [];
8044
+ const byEmail = identity2.email ? (await db.query({ hubMemberGrants: { $: { where: { emailNormalized: normalizeChapterEmail(identity2.email) }, limit: 1 } } })).hubMemberGrants ?? [] : [];
8045
+ return [...byUser, ...byEmail].find((row2) => row2.status === "active") ?? null;
8046
+ }
8047
+ async function resolvePortalMember(db, env, ctx, identity2) {
8048
+ try {
8049
+ if (ctx.chapter.mode === "hub") {
8050
+ const grant = await activeHubGrant(db, identity2);
8051
+ if (!grant || !identity2.email) return { status: "not-member" };
8052
+ return { status: "member", member: {
8053
+ memberId: String(grant.id),
8054
+ source: "hub-grant",
8055
+ email: identity2.email,
8056
+ ...text6(grant.firstName) ? { firstName: text6(grant.firstName) } : {},
8057
+ ...text6(grant.lastName) ? { lastName: text6(grant.lastName) } : {},
8058
+ tierId: null,
8059
+ memberSince: Number(grant.grantedAt) || null,
8060
+ row: grant
8061
+ } };
8062
+ }
8063
+ const session = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, identity2);
8064
+ if (!session) return { status: "not-member" };
8065
+ const app = session.source;
8066
+ if (!await chapterMembershipAccess(db, env, ctx, app)) return { status: "not-member", row: app };
8067
+ return { status: "member", member: {
8068
+ memberId: String(app.id),
8069
+ source: "application",
8070
+ email: String(app.email ?? identity2.email ?? ""),
8071
+ ...text6(app.firstName) ? { firstName: text6(app.firstName) } : {},
8072
+ ...text6(app.lastName) ? { lastName: text6(app.lastName) } : {},
8073
+ tierId: entitledTierId(app),
8074
+ memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
8075
+ row: app
8076
+ } };
8077
+ } catch {
8078
+ return { status: "unavailable" };
8079
+ }
8080
+ }
8081
+ function initialsOf(name2) {
8082
+ const words2 = name2.trim().split(/\s+/).filter(Boolean);
8083
+ const letters = words2.length > 1 ? [words2[0], words2[words2.length - 1]] : words2;
8084
+ return letters.map((word) => [...word][0].toUpperCase()).join("") || "?";
8085
+ }
8086
+ async function portalIdentity(db, ctx, member) {
8087
+ const profile = (await db.query({ memberProfiles: { $: { where: { id: member.memberId }, limit: 1 } } })).memberProfiles?.[0];
8088
+ let tier = null;
8089
+ if (member.tierId) {
8090
+ const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
8091
+ const match = group ? (await loadTiers(db, group)).find((candidate) => candidate.id === member.tierId) : void 0;
8092
+ tier = { id: member.tierId, name: match?.name ?? member.tierId };
8093
+ }
8094
+ const legal = [member.firstName, member.lastName].filter(Boolean).join(" ");
8095
+ const displayName = text6(profile?.preferredName) ?? (legal || member.email);
8096
+ const photoPath = text6(profile?.photoPath);
8097
+ const photoUrl = photoPath && db.storage && ctx.chapter.memberPortal?.photos ? await db.storage.sign(photoPath, 3600).catch(() => null) : null;
8098
+ return {
8099
+ memberId: member.memberId,
8100
+ tier,
8101
+ memberSince: member.memberSince,
8102
+ displayName,
8103
+ firstName: text6(profile?.preferredName)?.split(/\s+/)[0] ?? member.firstName ?? null,
8104
+ initials: initialsOf(displayName),
8105
+ location: text6(profile?.location) ?? null,
8106
+ photoUrl
8107
+ };
8108
+ }
8109
+
8110
+ // src/portal-me.ts
8111
+ var servesMembers = (ctx) => ctx.chapter.mode === "chapter" || Boolean(ctx.chapter.memberPortal || ctx.chapter.memberContent);
8112
+ async function hubMe(db, env, ctx, identity2, base) {
8113
+ if (!servesMembers(ctx)) return base;
8114
+ const result = await resolvePortalMember(db, env, ctx, identity2);
8115
+ if (result.status === "unavailable") return { ...base, memberAccess: null, portal: null };
8116
+ if (result.status !== "member") return { ...base, memberAccess: false, portal: null };
8117
+ return { ...base, memberAccess: true, portal: await identityOrNull(db, ctx, result.member) };
8118
+ }
8119
+ async function chapterPortalIdentity(db, ctx, memberAccess, app) {
8120
+ if (!memberAccess || !app || !ctx.chapter.memberPortal) return null;
8121
+ const text11 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8122
+ return identityOrNull(db, ctx, {
8123
+ memberId: String(app.id),
8124
+ source: "application",
8125
+ email: String(app.email ?? ""),
8126
+ ...text11(app.firstName) ? { firstName: text11(app.firstName) } : {},
8127
+ ...text11(app.lastName) ? { lastName: text11(app.lastName) } : {},
8128
+ tierId: entitledTierId(app),
8129
+ memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
8130
+ row: app
8131
+ });
8132
+ }
8133
+ async function identityOrNull(db, ctx, member) {
8134
+ return portalIdentity(db, ctx, member).catch(() => null);
8135
+ }
8136
+
7681
8137
  // src/worker-routes-health.ts
7682
8138
  var handleConfig = async (_req, url, env, ctx) => {
7683
8139
  if (url.pathname !== "/api/config") return null;
@@ -7692,13 +8148,13 @@ var handleHealth = async (_req, url, env, ctx) => {
7692
8148
  if (url.pathname !== "/api/health") return null;
7693
8149
  const response = json({ ok: true });
7694
8150
  response.headers.set("cache-control", "no-store");
7695
- for (const [name, value] of Object.entries({
8151
+ for (const [name2, value] of Object.entries({
7696
8152
  "x-odla-app-id": env.ODLA_APP_ID,
7697
8153
  "x-odla-chapter-id": ctx.chapter.id,
7698
8154
  "x-odla-data-environment": env.ODLA_ENV,
7699
8155
  "x-odla-runtime": env.ODLA_RUNTIME || "default"
7700
8156
  })) {
7701
- if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name, value);
8157
+ if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name2, value);
7702
8158
  }
7703
8159
  return response;
7704
8160
  };
@@ -7712,8 +8168,10 @@ var handleMe = async (req, url, env, ctx) => {
7712
8168
  const db = ctx.makeDb(env);
7713
8169
  const role = await ctx.roleFor(db, u);
7714
8170
  const superAdmin = await ctx.isSuperAdminEmail(db, u.email, u.userId);
7715
- const base = { authorized: superAdmin || isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
7716
- if (ctx.chapter.mode !== "chapter") return json(base);
8171
+ const admin = superAdmin || isAdminRole(role, ctx.auth);
8172
+ const secondFactorOk = !admin || adminSecondFactorAllows(ctx.auth.adminSecondFactor ?? "off", u.payload, u.email);
8173
+ const base = { authorized: admin && secondFactorOk, role, superAdmin, email: u.email ?? null, ...admin && !secondFactorOk ? { secondFactorRequired: true } : {} };
8174
+ if (ctx.chapter.mode !== "chapter") return json(await hubMe(db, env, ctx, u, base));
7717
8175
  let member;
7718
8176
  try {
7719
8177
  member = await memberSessionApplication(
@@ -7749,7 +8207,8 @@ var handleMe = async (req, url, env, ctx) => {
7749
8207
  } catch {
7750
8208
  }
7751
8209
  }
7752
- return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
8210
+ const portal = await chapterPortalIdentity(db, ctx, memberAccess, member?.source);
8211
+ return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, portal, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
7753
8212
  };
7754
8213
  var handleCrm = async (req, url, env, ctx) => {
7755
8214
  const crmBase = ctx.crmBase;
@@ -7994,9 +8453,9 @@ var MAX_BENEFITS = 24;
7994
8453
  var MAX_MARKDOWN = 65536;
7995
8454
  var MAX_LABEL = 160;
7996
8455
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
7997
- function rejectUnknownKeys(value, allowed2, path, errors) {
8456
+ function rejectUnknownKeys(value, allowed, path, errors) {
7998
8457
  for (const key3 of Object.keys(value)) {
7999
- if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8458
+ if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8000
8459
  }
8001
8460
  }
8002
8461
  function boundedString(value, path, errors, max, required = false) {
@@ -8008,10 +8467,10 @@ function boundedString(value, path, errors, max, required = false) {
8008
8467
  errors.push(`${path} must be a string`);
8009
8468
  return void 0;
8010
8469
  }
8011
- const text10 = value.trim();
8012
- if (required && !text10) errors.push(`${path} is required`);
8013
- if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8014
- return text10;
8470
+ const text11 = value.trim();
8471
+ if (required && !text11) errors.push(`${path} is required`);
8472
+ if (text11.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8473
+ return text11;
8015
8474
  }
8016
8475
  function validateMembershipPageDocument(input, options = {}) {
8017
8476
  const errors = [];
@@ -8060,8 +8519,8 @@ function validateMembershipPageDocument(input, options = {}) {
8060
8519
  else {
8061
8520
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
8062
8521
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
8063
- const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8064
- return text10 ? [text10] : [];
8522
+ const text11 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8523
+ return text11 ? [text11] : [];
8065
8524
  });
8066
8525
  }
8067
8526
  }
@@ -8152,10 +8611,10 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
8152
8611
  ]);
8153
8612
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
8154
8613
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8155
- function unknownKeys(value, allowed2, path, errors) {
8156
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8614
+ function unknownKeys(value, allowed, path, errors) {
8615
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8157
8616
  }
8158
- function text6(value, path, errors, max = 65536) {
8617
+ function text7(value, path, errors, max = 65536) {
8159
8618
  if (typeof value !== "string" || !value.trim()) {
8160
8619
  errors.push(`${path} is required`);
8161
8620
  return "";
@@ -8178,11 +8637,11 @@ function tiersOf(value, errors) {
8178
8637
  return [];
8179
8638
  }
8180
8639
  unknownKeys(item, TIER_KEYS2, path, errors);
8181
- const id7 = text6(item.id, `${path}.id`, errors, 80);
8640
+ const id7 = text7(item.id, `${path}.id`, errors, 80);
8182
8641
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8183
8642
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
8184
8643
  seen.add(id7);
8185
- const name = text6(item.name, `${path}.name`, errors, 160);
8644
+ const name2 = text7(item.name, `${path}.name`, errors, 160);
8186
8645
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
8187
8646
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
8188
8647
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -8200,10 +8659,10 @@ function tiersOf(value, errors) {
8200
8659
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
8201
8660
  errors.push(`${path}.interval must be year or month`);
8202
8661
  }
8203
- const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
8662
+ const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
8204
8663
  return [{
8205
8664
  id: id7,
8206
- name,
8665
+ name: name2,
8207
8666
  available: item.available === true,
8208
8667
  sortOrder: Number(item.sortOrder) || 0,
8209
8668
  priceCents: Number(item.priceCents) || 0,
@@ -8246,7 +8705,7 @@ function policyOf(value, errors, tiers) {
8246
8705
  errors.push("document.policy must be an object");
8247
8706
  return void 0;
8248
8707
  }
8249
- const allowed2 = /* @__PURE__ */ new Set([
8708
+ const allowed = /* @__PURE__ */ new Set([
8250
8709
  "disclaimerText",
8251
8710
  "refundPolicyText",
8252
8711
  "merchantDisclosureText",
@@ -8254,11 +8713,11 @@ function policyOf(value, errors, tiers) {
8254
8713
  "commitmentText",
8255
8714
  "normsText"
8256
8715
  ]);
8257
- unknownKeys(input, /* @__PURE__ */ new Set([...allowed2, "tierPolicies"]), "document.policy", errors);
8716
+ unknownKeys(input, /* @__PURE__ */ new Set([...allowed, "tierPolicies"]), "document.policy", errors);
8258
8717
  const result = {};
8259
- for (const key3 of allowed2) {
8718
+ for (const key3 of allowed) {
8260
8719
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
8261
- result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
8720
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
8262
8721
  }
8263
8722
  const tierPolicies = {};
8264
8723
  if (input.tierPolicies !== void 0) {
@@ -8273,8 +8732,8 @@ function policyOf(value, errors, tiers) {
8273
8732
  }
8274
8733
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
8275
8734
  tierPolicies[id7] = {
8276
- refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8277
- ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8735
+ refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8736
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8278
8737
  };
8279
8738
  }
8280
8739
  }
@@ -8314,9 +8773,9 @@ function validateSignupControlDocument(input) {
8314
8773
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
8315
8774
  const source = recordOf(root.source);
8316
8775
  const target = recordOf(root.target);
8317
- const appId = text6(source?.appId, "document.source.appId", errors, 63);
8318
- const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
8319
- const environment = text6(target?.environment, "document.target.environment", errors, 63);
8776
+ const appId = text7(source?.appId, "document.source.appId", errors, 63);
8777
+ const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
8778
+ const environment = text7(target?.environment, "document.target.environment", errors, 63);
8320
8779
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
8321
8780
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
8322
8781
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -8457,14 +8916,45 @@ async function memberContentDigest(document) {
8457
8916
  return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
8458
8917
  }
8459
8918
 
8919
+ // src/member-content-kinds.ts
8920
+ var MEMBER_PAGE_FEATURES = ["course", "share"];
8921
+ var MEMBER_PAGE_ICONS = [
8922
+ "home",
8923
+ "course",
8924
+ "library",
8925
+ "deals",
8926
+ "training",
8927
+ "events",
8928
+ "members",
8929
+ "share",
8930
+ "profile",
8931
+ "document",
8932
+ "calendar",
8933
+ "chat",
8934
+ "star"
8935
+ ];
8936
+ var MEMBER_PAGE_TAGS = ["new"];
8937
+ function composeMemberSections(parent, chapter, pages) {
8938
+ const parentDeclared = new Map((parent?.sections ?? []).map((section) => [section.id, section]));
8939
+ const out = /* @__PURE__ */ new Map();
8940
+ for (const section of chapter?.sections ?? []) {
8941
+ const label = section.label ?? parentDeclared.get(section.id)?.label;
8942
+ out.set(section.id, { id: section.id, ...label ? { label } : {} });
8943
+ }
8944
+ for (const section of parent?.sections ?? []) if (!out.has(section.id)) out.set(section.id, section);
8945
+ const named = [...new Set(pages.map((page) => page.navigation.section))];
8946
+ for (const id7 of named.filter((id8) => !out.has(id8)).sort((a, b) => a.localeCompare(b))) out.set(id7, { id: id7, label: id7 });
8947
+ return [...out.values()].filter((section) => named.includes(section.id));
8948
+ }
8949
+
8460
8950
  // src/member-content-validation-helpers.ts
8461
8951
  var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
8462
8952
  var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8463
- var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
8953
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides", "sections"]);
8464
8954
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
8465
8955
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
8466
- var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
8467
- var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
8956
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell", "kind", "placeholder", "feature"]);
8957
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId", "tag", "icon"]);
8468
8958
  var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
8469
8959
  var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
8470
8960
  var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
@@ -8473,8 +8963,8 @@ var MAX_PAGES = 100;
8473
8963
  var MAX_MARKDOWN2 = 65536;
8474
8964
  var MAX_TEXT = 240;
8475
8965
  var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8476
- function unknownKeys2(value, allowed2, path, errors) {
8477
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8966
+ function unknownKeys2(value, allowed, path, errors) {
8967
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8478
8968
  }
8479
8969
  function textValue(value, path, errors, max = MAX_TEXT, required = false) {
8480
8970
  if (value === void 0) {
@@ -8514,13 +9004,140 @@ function offerValue(value, path, errors, tiers, audience) {
8514
9004
  };
8515
9005
  }
8516
9006
 
9007
+ // src/member-content-validation-v2.ts
9008
+ var SECTION_KEYS = /* @__PURE__ */ new Set(["id", "label"]);
9009
+ var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set(["availableYear", "badge", "actionLabel"]);
9010
+ var FEATURE_KEYS = /* @__PURE__ */ new Set(["name", "ref"]);
9011
+ var MAX_SECTIONS = 24;
9012
+ var V2_ROOT_KEYS = ["sections"];
9013
+ var V2_PAGE_KEYS = ["kind", "placeholder", "feature"];
9014
+ var V2_NAV_KEYS = ["tag", "icon"];
9015
+ function rejectV2Fields(raw2, keys3, path, errors) {
9016
+ for (const key3 of keys3) if (raw2[key3] !== void 0) errors.push(`${path}.${key3} requires schemaVersion 2`);
9017
+ }
9018
+ function sectionsValue(value, errors) {
9019
+ if (value === void 0) return void 0;
9020
+ if (!Array.isArray(value)) {
9021
+ errors.push("document.sections must be an array");
9022
+ return void 0;
9023
+ }
9024
+ if (value.length > MAX_SECTIONS) errors.push(`document.sections supports at most ${MAX_SECTIONS} entries`);
9025
+ const seen = /* @__PURE__ */ new Set();
9026
+ const sections = [];
9027
+ for (const [index2, item] of value.slice(0, MAX_SECTIONS).entries()) {
9028
+ const path = `document.sections[${index2}]`;
9029
+ const raw2 = record2(item);
9030
+ if (!raw2) {
9031
+ errors.push(`${path} must be an object`);
9032
+ continue;
9033
+ }
9034
+ unknownKeys2(raw2, SECTION_KEYS, path, errors);
9035
+ const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
9036
+ if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9037
+ if (seen.has(id7)) errors.push(`${path}.id duplicates section "${id7}"`);
9038
+ seen.add(id7);
9039
+ const label = textValue(raw2.label, `${path}.label`, errors, 120);
9040
+ sections.push({ id: id7, ...label ? { label } : {} });
9041
+ }
9042
+ return sections;
9043
+ }
9044
+ var oneOf = (values, value) => typeof value === "string" && values.includes(value);
9045
+ function navigationV2(raw2, path, errors) {
9046
+ const out = {};
9047
+ if (raw2.tag !== void 0) {
9048
+ if (oneOf(MEMBER_PAGE_TAGS, raw2.tag)) out.tag = raw2.tag;
9049
+ else errors.push(`${path}.tag must be one of ${MEMBER_PAGE_TAGS.join(", ")}`);
9050
+ }
9051
+ if (raw2.icon !== void 0) {
9052
+ if (oneOf(MEMBER_PAGE_ICONS, raw2.icon)) out.icon = raw2.icon;
9053
+ else errors.push(`${path}.icon must be one of ${MEMBER_PAGE_ICONS.join(", ")}`);
9054
+ }
9055
+ return out;
9056
+ }
9057
+ function placeholderValue(value, path, errors) {
9058
+ const raw2 = record2(value);
9059
+ if (!raw2) {
9060
+ errors.push(`${path} must be an object`);
9061
+ return {};
9062
+ }
9063
+ unknownKeys2(raw2, PLACEHOLDER_KEYS, path, errors);
9064
+ const year = raw2.availableYear;
9065
+ if (year !== void 0 && (!Number.isInteger(year) || Number(year) < 2e3 || Number(year) > 2100)) {
9066
+ errors.push(`${path}.availableYear must be a year from 2000 to 2100`);
9067
+ }
9068
+ const badge = textValue(raw2.badge, `${path}.badge`, errors, 60);
9069
+ const actionLabel = textValue(raw2.actionLabel, `${path}.actionLabel`, errors, 80);
9070
+ return {
9071
+ ...Number.isInteger(year) ? { availableYear: Number(year) } : {},
9072
+ ...badge ? { badge } : {},
9073
+ ...actionLabel ? { actionLabel } : {}
9074
+ };
9075
+ }
9076
+ function featureValue(value, path, errors) {
9077
+ const raw2 = record2(value);
9078
+ if (!raw2) {
9079
+ errors.push(`${path} must be an object`);
9080
+ return void 0;
9081
+ }
9082
+ unknownKeys2(raw2, FEATURE_KEYS, path, errors);
9083
+ if (!oneOf(MEMBER_PAGE_FEATURES, raw2.name)) {
9084
+ errors.push(`${path}.name must be one of ${MEMBER_PAGE_FEATURES.join(", ")}`);
9085
+ return void 0;
9086
+ }
9087
+ const ref = textValue(raw2.ref, `${path}.ref`, errors, 80);
9088
+ if (ref && !SLUG2.test(ref)) errors.push(`${path}.ref must be a lowercase slug`);
9089
+ return { name: raw2.name, ...ref ? { ref } : {} };
9090
+ }
9091
+ function pageKindValue(raw2, path, errors) {
9092
+ const kind = raw2.kind ?? "markdown";
9093
+ if (kind !== "markdown" && kind !== "placeholder" && kind !== "feature") {
9094
+ errors.push(`${path}.kind must be markdown, placeholder or feature`);
9095
+ return {};
9096
+ }
9097
+ if (kind !== "placeholder" && raw2.placeholder !== void 0) errors.push(`${path}.placeholder requires kind placeholder`);
9098
+ if (kind !== "feature" && raw2.feature !== void 0) errors.push(`${path}.feature requires kind feature`);
9099
+ if (kind === "markdown" && raw2.bodyMarkdown === void 0) errors.push(`${path}.bodyMarkdown is required`);
9100
+ if (kind === "feature" && raw2.bodyMarkdown !== void 0) errors.push(`${path}.bodyMarkdown is not supported on a feature page`);
9101
+ const out = raw2.kind === void 0 ? {} : { kind };
9102
+ if (kind === "placeholder") out.placeholder = placeholderValue(raw2.placeholder ?? {}, `${path}.placeholder`, errors);
9103
+ if (kind === "feature") {
9104
+ if (raw2.feature === void 0) errors.push(`${path}.feature is required for kind feature`);
9105
+ else {
9106
+ const feature = featureValue(raw2.feature, `${path}.feature`, errors);
9107
+ if (feature) out.feature = feature;
9108
+ }
9109
+ }
9110
+ return out;
9111
+ }
9112
+ function duplicateFeatures(pages, errors) {
9113
+ const seen = /* @__PURE__ */ new Set();
9114
+ for (const [index2, page] of pages.entries()) {
9115
+ if (!page.feature) continue;
9116
+ const key3 = `${page.feature.name}:${page.feature.ref ?? ""}`;
9117
+ if (seen.has(key3)) errors.push(`document.pages[${index2}].feature duplicates ${page.feature.name}${page.feature.ref ? ` "${page.feature.ref}"` : ""}`);
9118
+ seen.add(key3);
9119
+ }
9120
+ }
9121
+ function sectionReferences(pages, sections, parentPages, errors) {
9122
+ if (!sections) return;
9123
+ const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), ...parentPages.map((page) => page.navigation.section)]);
9124
+ for (const [index2, page] of pages.entries()) {
9125
+ if (!known.has(page.navigation.section)) {
9126
+ errors.push(`document.pages[${index2}].navigation.section references undeclared section "${page.navigation.section}"`);
9127
+ }
9128
+ }
9129
+ }
9130
+
8517
9131
  // src/member-content-validation.ts
8518
9132
  function validateMemberContentDocument(input, options = {}) {
8519
9133
  const errors = [];
8520
9134
  const root = record2(input);
8521
9135
  if (!root) return { ok: false, errors: ["document must be an object"] };
8522
9136
  unknownKeys2(root, ROOT_KEYS3, "document", errors);
8523
- if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
9137
+ if (root.schemaVersion !== 1 && root.schemaVersion !== 2) errors.push("document.schemaVersion must be 1 or 2");
9138
+ const v2 = root.schemaVersion === 2;
9139
+ if (!v2) rejectV2Fields(root, V2_ROOT_KEYS, "document", errors);
9140
+ const sections = v2 ? sectionsValue(root.sections, errors) : void 0;
8524
9141
  if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
8525
9142
  if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
8526
9143
  const sourceRaw = record2(root.source);
@@ -8565,6 +9182,8 @@ function validateMemberContentDocument(input, options = {}) {
8565
9182
  continue;
8566
9183
  }
8567
9184
  unknownKeys2(raw2, PAGE_KEYS, path, errors);
9185
+ if (!v2) rejectV2Fields(raw2, V2_PAGE_KEYS, path, errors);
9186
+ const kindFields = v2 ? pageKindValue(raw2, path, errors) : {};
8568
9187
  const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
8569
9188
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
8570
9189
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
@@ -8579,13 +9198,19 @@ function validateMemberContentDocument(input, options = {}) {
8579
9198
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
8580
9199
  else {
8581
9200
  unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
9201
+ if (!v2) rejectV2Fields(navRaw, V2_NAV_KEYS, `${path}.navigation`, errors);
8582
9202
  const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
8583
9203
  const order2 = navRaw.order;
8584
9204
  if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
8585
9205
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
8586
9206
  }
8587
9207
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
8588
- navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
9208
+ navigation = {
9209
+ section,
9210
+ order: Number.isInteger(order2) ? Number(order2) : index2,
9211
+ ...parentId ? { parentId } : {},
9212
+ ...v2 ? navigationV2(navRaw, `${path}.navigation`, errors) : {}
9213
+ };
8589
9214
  }
8590
9215
  const audienceRaw = record2(raw2.audience);
8591
9216
  let audience = { kind: "all-members" };
@@ -8611,7 +9236,7 @@ function validateMemberContentDocument(input, options = {}) {
8611
9236
  }
8612
9237
  const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
8613
9238
  const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
8614
- const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
9239
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, !v2);
8615
9240
  let upsell;
8616
9241
  if (raw2.upsell !== void 0) {
8617
9242
  const upsellRaw = record2(raw2.upsell);
@@ -8628,7 +9253,22 @@ function validateMemberContentDocument(input, options = {}) {
8628
9253
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
8629
9254
  }
8630
9255
  }
8631
- pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
9256
+ pages.push({
9257
+ id: id7,
9258
+ slug,
9259
+ title,
9260
+ ...summary ? { summary } : {},
9261
+ navigation,
9262
+ audience,
9263
+ ...teaserMarkdown ? { teaserMarkdown } : {},
9264
+ ...bodyMarkdown !== void 0 ? { bodyMarkdown } : {},
9265
+ ...upsell ? { upsell } : {},
9266
+ ...kindFields
9267
+ });
9268
+ }
9269
+ if (v2) {
9270
+ duplicateFeatures(pages, errors);
9271
+ sectionReferences(pages, sections, options.parentPages ?? [], errors);
8632
9272
  }
8633
9273
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
8634
9274
  for (const [index2, page] of pages.entries()) {
@@ -8675,21 +9315,21 @@ function validateMemberContentDocument(input, options = {}) {
8675
9315
  }
8676
9316
  if (errors.length) return { ok: false, errors };
8677
9317
  return { ok: true, value: {
8678
- schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
9318
+ schemaVersion: v2 ? 2 : 1,
8679
9319
  revision: Number(root.revision),
8680
9320
  generatedAt: Number(root.generatedAt),
8681
9321
  source,
8682
9322
  ...target ? { target } : {},
8683
9323
  pages,
8684
- ...upsellOverrides ? { upsellOverrides } : {}
9324
+ ...upsellOverrides ? { upsellOverrides } : {},
9325
+ ...sections ? { sections } : {}
8685
9326
  } };
8686
9327
  }
8687
9328
 
8688
9329
  // src/member-content.ts
8689
- var MEMBER_CONTENT_SCHEMA_VERSION = 1;
8690
9330
  var MEMBER_CONTENT_PATH = "/api/member-content";
8691
9331
  var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
8692
- 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);
9332
+ 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);
8693
9333
  function composeMemberContent(parent, chapter) {
8694
9334
  const pages = [];
8695
9335
  const parentPages = /* @__PURE__ */ new Map();
@@ -8708,20 +9348,24 @@ function composeMemberContent(parent, chapter) {
8708
9348
  }
8709
9349
  pages.push({ ...page, source: chapter.source });
8710
9350
  }
9351
+ const sections = composeMemberSections(parent, chapter, pages);
9352
+ const rank = new Map(sections.map((section, index2) => [section.id, index2]));
8711
9353
  return {
8712
- pages: pages.sort(pageOrder),
9354
+ pages: pages.sort(pageOrder(rank)),
9355
+ sections,
8713
9356
  parentRevision: parent?.revision ?? null,
8714
9357
  chapterRevision: chapter?.revision ?? null
8715
9358
  };
8716
9359
  }
8717
- var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
9360
+ var memberPageAllowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
8718
9361
  function projectMemberContent(content, viewer, tiers) {
8719
9362
  const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
8720
9363
  return {
8721
9364
  viewer: { ...viewer },
8722
9365
  revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
9366
+ sections: content.sections ?? composeMemberSections(null, null, content.pages),
8723
9367
  pages: content.pages.map((page) => {
8724
- const canRead = allowed(page, viewer);
9368
+ const canRead = memberPageAllowed(page, viewer);
8725
9369
  const offer = !canRead ? page.upsell?.offer : void 0;
8726
9370
  const tier = offer ? authority.get(offer.tierId) : void 0;
8727
9371
  const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
@@ -8734,8 +9378,11 @@ function projectMemberContent(content, viewer, tiers) {
8734
9378
  audience: page.audience,
8735
9379
  source: page.source,
8736
9380
  access: canRead ? "allowed" : "locked",
9381
+ kind: page.kind ?? "markdown",
9382
+ ...page.placeholder ? { placeholder: page.placeholder } : {},
9383
+ ...page.feature ? { feature: page.feature } : {},
8737
9384
  ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
8738
- ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
9385
+ ...canRead && page.bodyMarkdown !== void 0 ? { bodyMarkdown: page.bodyMarkdown } : {},
8739
9386
  ...upsell ? { upsell } : {}
8740
9387
  };
8741
9388
  })
@@ -8747,33 +9394,33 @@ var control = (row2) => row2 && row2.document && typeof row2.document === "objec
8747
9394
  var local2 = (row2) => row2 && row2.document && typeof row2.document === "object" ? row2 : null;
8748
9395
  async function exists(db, ns, where) {
8749
9396
  if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
8750
- const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
8751
- return rows5.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
9397
+ const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
9398
+ return rows6.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
8752
9399
  }
8753
9400
  async function loadActiveMemberContentControl(db, targetId, environment) {
8754
9401
  const where = { targetId, environment, active: true };
8755
9402
  if (!await exists(db, "memberContentControls", where)) return null;
8756
- const rows5 = (await db.query({ memberContentControls: { $: {
9403
+ const rows6 = (await db.query({ memberContentControls: { $: {
8757
9404
  where,
8758
9405
  order: { revision: "desc" },
8759
9406
  limit: 1
8760
9407
  } } })).memberContentControls;
8761
- return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
9408
+ return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
8762
9409
  }
8763
9410
  async function loadLatestMemberContentControl(db, targetId, environment) {
8764
9411
  const where = { targetId, environment };
8765
9412
  if (!await exists(db, "memberContentControls", where)) return null;
8766
- const rows5 = (await db.query({ memberContentControls: { $: {
9413
+ const rows6 = (await db.query({ memberContentControls: { $: {
8767
9414
  where,
8768
9415
  order: { revision: "desc" },
8769
9416
  limit: 1
8770
9417
  } } })).memberContentControls;
8771
- return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
9418
+ return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
8772
9419
  }
8773
9420
  async function loadLocalMemberContent(db, chapterId, environment) {
8774
9421
  const id7 = `member-content-local:${environment}`;
8775
- const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
8776
- return local2(Array.isArray(rows5) ? rows5[0] : void 0);
9422
+ const rows6 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
9423
+ return local2(Array.isArray(rows6) ? rows6[0] : void 0);
8777
9424
  }
8778
9425
  async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
8779
9426
  const { document, digest } = envelope;
@@ -8820,7 +9467,8 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
8820
9467
  var ADMIN_PATH = "/api/admin/member-content";
8821
9468
  var BODY_CAP = 1e6;
8822
9469
  var reject4 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
8823
- async function groupAndTiers(db, chapterId) {
9470
+ async function groupAndTiers(db, chapterId, mode = "chapter") {
9471
+ if (mode === "hub") return { group: null, tiers: [] };
8824
9472
  const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
8825
9473
  const group = Array.isArray(groups) ? groups[0] : void 0;
8826
9474
  if (!group) return { group: null, tiers: [] };
@@ -8857,7 +9505,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
8857
9505
  if (document.source.appId !== settings.sourceId) return reject4("wrong_source", 403);
8858
9506
  if (document.target?.chapterId !== ctx.chapter.id) return reject4("wrong_target", 409);
8859
9507
  if (document.target.environment !== env.ODLA_ENV) return reject4("wrong_environment", 409);
8860
- const { tiers } = await groupAndTiers(db, ctx.chapter.id);
9508
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode);
8861
9509
  const checked = validateMemberContentDocument(document, {
8862
9510
  sourceType: "parent",
8863
9511
  sourceAppId: settings.sourceId,
@@ -8911,17 +9559,13 @@ var handleMemberContent = async (req, url, env, ctx) => {
8911
9559
  const user = await ctx.verifyUser(req, env);
8912
9560
  if (!user) return json({ error: "unauthorized" }, 401);
8913
9561
  const db = ctx.makeDb(env);
8914
- let tierId;
8915
- try {
8916
- const member = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, { userId: user.userId, email: user.email });
8917
- if (!member || !await chapterMembershipAccess(db, env, ctx, member.source)) return json({ error: "member access required" }, 403);
8918
- tierId = typeof member.source.tierId === "string" ? member.source.tierId : null;
8919
- } catch {
8920
- return json({ error: "membership access unavailable" }, 503);
8921
- }
9562
+ const resolved = await resolvePortalMember(db, env, ctx, user);
9563
+ if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
9564
+ if (resolved.status !== "member") return json({ error: "member access required" }, 403);
9565
+ const tierId = resolved.member.tierId;
8922
9566
  const [{ parent, chapter }, { group, tiers }] = await Promise.all([
8923
9567
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
8924
- groupAndTiers(db, ctx.chapter.id)
9568
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
8925
9569
  ]);
8926
9570
  const effective = composeMemberContent(parent?.document, chapter?.document);
8927
9571
  let offerable = tiers;
@@ -8940,7 +9584,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
8940
9584
  if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
8941
9585
  const [{ parent, chapter }, { tiers }] = await Promise.all([
8942
9586
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
8943
- groupAndTiers(db, ctx.chapter.id)
9587
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
8944
9588
  ]);
8945
9589
  if (req.method === "GET") {
8946
9590
  const effective = composeMemberContent(parent?.document, chapter?.document);
@@ -8948,7 +9592,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
8948
9592
  parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
8949
9593
  chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
8950
9594
  effective,
8951
- tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
9595
+ tiers: tiers.map(({ id: id7, name: name2, priceCents, active }) => ({ id: id7, name: name2, priceCents, active })),
8952
9596
  localAuthoring: ctx.chapter.memberContent.localAuthoring
8953
9597
  });
8954
9598
  }
@@ -8975,6 +9619,8 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
8975
9619
  parentPages: parent?.document.pages ?? []
8976
9620
  });
8977
9621
  if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
9622
+ const tiered = ctx.chapter.mode === "hub" ? checked.value.pages.findIndex((page) => page.audience.kind === "tiers") : -1;
9623
+ if (tiered >= 0) return json({ error: "invalid member content", errors: [`document.pages[${tiered}].audience must be all-members: a hub has no tiers`] }, 400);
8978
9624
  try {
8979
9625
  const saved = await saveLocalMemberContent(
8980
9626
  db,
@@ -8996,13 +9642,521 @@ function withLocalAuthority(value, revision, generatedAt, appId) {
8996
9642
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
8997
9643
  return {
8998
9644
  ...value,
8999
- schemaVersion: 1,
9645
+ // The author chooses the contract version; anything else defaults to 1.
9646
+ schemaVersion: value.schemaVersion === 2 ? 2 : 1,
9000
9647
  revision,
9001
9648
  generatedAt,
9002
9649
  source: { type: "chapter", appId }
9003
9650
  };
9004
9651
  }
9005
9652
 
9653
+ // src/portal-http.ts
9654
+ async function readJsonObject(req, maxBytes) {
9655
+ const text11 = await readBoundedRequestText(req, maxBytes);
9656
+ if (text11 instanceof Response) return text11;
9657
+ try {
9658
+ const value = JSON.parse(text11);
9659
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
9660
+ } catch {
9661
+ }
9662
+ return json({ error: "invalid JSON body" }, 400);
9663
+ }
9664
+
9665
+ // src/worker-routes-hub-members.ts
9666
+ var HUB_MEMBERS_PATH = "/api/admin/hub-members";
9667
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9668
+ async function grantId(emailNormalized) {
9669
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(emailNormalized));
9670
+ return `hub-member:${Array.from(new Uint8Array(digest).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
9671
+ }
9672
+ var view = (row2) => ({
9673
+ id: row2.id,
9674
+ email: row2.emailNormalized,
9675
+ firstName: row2.firstName ?? null,
9676
+ lastName: row2.lastName ?? null,
9677
+ status: row2.status,
9678
+ grantedBy: row2.grantedBy,
9679
+ grantedAt: row2.grantedAt,
9680
+ revokedBy: row2.revokedBy ?? null,
9681
+ revokedAt: row2.revokedAt ?? null,
9682
+ revision: row2.revision
9683
+ });
9684
+ var name = (value) => typeof value === "string" && value.trim() ? value.trim().slice(0, 80) : void 0;
9685
+ var handleAdminHubMembers = async (req, url, env, ctx) => {
9686
+ if (url.pathname !== HUB_MEMBERS_PATH && !url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) return null;
9687
+ if (ctx.chapter.mode !== "hub" || !servesMembers(ctx)) return json({ error: "not found" }, 404);
9688
+ const user = await ctx.verifyUser(req, env);
9689
+ if (!user) return json({ error: "unauthorized" }, 401);
9690
+ const db = ctx.makeDb(env);
9691
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
9692
+ const actor = user.email ?? user.userId;
9693
+ if (req.method === "GET" && url.pathname === HUB_MEMBERS_PATH) {
9694
+ const rows6 = (await db.query({ hubMemberGrants: { $: { limit: 1e3 } } })).hubMemberGrants ?? [];
9695
+ return json({ members: rows6.map(view).sort((a, b) => Number(b.grantedAt) - Number(a.grantedAt)) });
9696
+ }
9697
+ if (req.method === "POST" && url.pathname === HUB_MEMBERS_PATH) {
9698
+ const body = await readJsonObject(req, 8192);
9699
+ if (body instanceof Response) return body;
9700
+ const email = typeof body.email === "string" ? body.email.trim() : "";
9701
+ if (!EMAIL.test(email)) return json({ error: "a valid email is required" }, 400);
9702
+ const emailNormalized = normalizeChapterEmail(email);
9703
+ const id7 = await grantId(emailNormalized);
9704
+ const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
9705
+ if (current?.status === "active") return json({ member: view(current), status: "already-active" });
9706
+ const revision = Number(current?.revision ?? 0) + 1;
9707
+ const attrs = {
9708
+ id: id7,
9709
+ emailNormalized,
9710
+ status: "active",
9711
+ grantedBy: actor,
9712
+ grantedAt: Date.now(),
9713
+ revision,
9714
+ ...name(body.firstName) ? { firstName: name(body.firstName) } : {},
9715
+ ...name(body.lastName) ? { lastName: name(body.lastName) } : {}
9716
+ };
9717
+ const ops = [{ t: "update", ns: "hubMemberGrants", id: id7, attrs }];
9718
+ if (current) ops.push({ t: "retract", ns: "hubMemberGrants", id: id7, attrs: ["revokedBy", "revokedAt"] });
9719
+ try {
9720
+ 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 }] });
9721
+ } catch {
9722
+ return json({ error: "the roster changed; reload and try again" }, 409);
9723
+ }
9724
+ return json({ member: view({ ...current, ...attrs, revokedBy: void 0, revokedAt: void 0 }), status: current ? "re-granted" : "granted" }, current ? 200 : 201);
9725
+ }
9726
+ if (req.method === "DELETE" && url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) {
9727
+ const id7 = decodeURIComponent(url.pathname.slice(HUB_MEMBERS_PATH.length + 1));
9728
+ const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
9729
+ if (!current) return json({ error: "not found" }, 404);
9730
+ if (current.status !== "active") return json({ member: view(current), status: "already-revoked" });
9731
+ const attrs = { status: "revoked", revokedBy: actor, revokedAt: Date.now(), revision: Number(current.revision) + 1 };
9732
+ try {
9733
+ await db.transact([{ t: "update", ns: "hubMemberGrants", id: id7, attrs }], {
9734
+ mutationId: `hub-member-revoke:${id7}:${attrs.revision}`,
9735
+ guards: [{ ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } }]
9736
+ });
9737
+ } catch {
9738
+ return json({ error: "the roster changed; reload and try again" }, 409);
9739
+ }
9740
+ return json({ member: view({ ...current, ...attrs }), status: "revoked" });
9741
+ }
9742
+ return json({ error: "method not allowed" }, 405);
9743
+ };
9744
+
9745
+ // src/image-metadata.ts
9746
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
9747
+ var PNG_KEEP = /* @__PURE__ */ new Set(["IHDR", "PLTE", "IDAT", "IEND", "tRNS", "gAMA", "cHRM", "sRGB", "sBIT", "bKGD", "pHYs"]);
9748
+ var JPEG_KEEP = (marker) => marker === 224 || marker === 238 || marker === 219 || marker === 196 || marker === 221 || marker >= 192 && marker <= 207 && marker !== 200;
9749
+ var u16 = (b, i) => b[i] << 8 | b[i + 1];
9750
+ var u32 = (b, i) => (b[i] << 24 >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
9751
+ function concat(parts) {
9752
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
9753
+ let offset = 0;
9754
+ for (const part of parts) {
9755
+ out.set(part, offset);
9756
+ offset += part.length;
9757
+ }
9758
+ return out;
9759
+ }
9760
+ function cleanPng(bytes) {
9761
+ const parts = [bytes.subarray(0, 8)];
9762
+ let width = 0, height = 0, offset = 8, ended = false;
9763
+ while (offset + 12 <= bytes.length) {
9764
+ const length = u32(bytes, offset);
9765
+ const end = offset + 12 + length;
9766
+ if (end > bytes.length) return null;
9767
+ const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
9768
+ if (type === "IHDR") {
9769
+ width = u32(bytes, offset + 8);
9770
+ height = u32(bytes, offset + 12);
9771
+ }
9772
+ if (PNG_KEEP.has(type)) parts.push(bytes.subarray(offset, end));
9773
+ offset = end;
9774
+ if (type === "IEND") {
9775
+ ended = true;
9776
+ break;
9777
+ }
9778
+ }
9779
+ if (!ended || !width || !height) return null;
9780
+ return { contentType: "image/png", extension: "png", width, height, bytes: concat(parts) };
9781
+ }
9782
+ function cleanJpeg(bytes) {
9783
+ const parts = [bytes.subarray(0, 2)];
9784
+ let width = 0, height = 0, offset = 2;
9785
+ while (offset + 4 <= bytes.length) {
9786
+ if (bytes[offset] !== 255) return null;
9787
+ const marker = bytes[offset + 1];
9788
+ if (marker === 255) {
9789
+ offset += 1;
9790
+ continue;
9791
+ }
9792
+ const length = u16(bytes, offset + 2);
9793
+ const end = offset + 2 + length;
9794
+ if (length < 2 || end > bytes.length) return null;
9795
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
9796
+ height = u16(bytes, offset + 5);
9797
+ width = u16(bytes, offset + 7);
9798
+ }
9799
+ if (marker === 218) {
9800
+ parts.push(bytes.subarray(offset));
9801
+ return width && height ? { contentType: "image/jpeg", extension: "jpg", width, height, bytes: concat(parts) } : null;
9802
+ }
9803
+ if (JPEG_KEEP(marker)) parts.push(bytes.subarray(offset, end));
9804
+ offset = end;
9805
+ }
9806
+ return null;
9807
+ }
9808
+ function cleanImage(bytes) {
9809
+ if (bytes.length > 8 && PNG_SIGNATURE.every((byte, index2) => bytes[index2] === byte)) return cleanPng(bytes);
9810
+ if (bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return cleanJpeg(bytes);
9811
+ return null;
9812
+ }
9813
+
9814
+ // src/member-profile.ts
9815
+ var MEMBER_PROFILE_FIELDS = {
9816
+ preferredName: "person.preferredName",
9817
+ location: "person.region",
9818
+ prompts: "person.profilePrompts",
9819
+ introducedByText: "person.introducedByText"
9820
+ };
9821
+ var MAX_PROMPT_ANSWER = 280;
9822
+ var clean = (value) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
9823
+ function optionalText(body, key3, max, errors) {
9824
+ if (!(key3 in body)) return void 0;
9825
+ const value = body[key3];
9826
+ if (value === null || value === "") return null;
9827
+ if (typeof value !== "string") {
9828
+ errors.push(`${key3} must be a string`);
9829
+ return void 0;
9830
+ }
9831
+ const text11 = clean(value);
9832
+ if (text11.length > max) errors.push(`${key3} must be at most ${max} characters`);
9833
+ return text11 || null;
9834
+ }
9835
+ function validateProfilePatch(body, portal) {
9836
+ const errors = [];
9837
+ for (const key3 of Object.keys(body)) {
9838
+ if (key3 === "expectedRevision") continue;
9839
+ if (!(key3 in MEMBER_PROFILE_FIELDS)) errors.push(`${key3} is not an editable profile field`);
9840
+ else if (!MEMBER_SELF_EDITABLE_FIELDS.has(MEMBER_PROFILE_FIELDS[key3])) errors.push(`${key3} is read-only`);
9841
+ }
9842
+ const patch = {};
9843
+ const preferredName = optionalText(body, "preferredName", 80, errors);
9844
+ if (preferredName !== void 0) patch.preferredName = preferredName;
9845
+ const location = optionalText(body, "location", 80, errors);
9846
+ if (location !== void 0) patch.location = location;
9847
+ const introducedByText = optionalText(body, "introducedByText", 120, errors);
9848
+ if (introducedByText !== void 0) patch.introducedByText = introducedByText;
9849
+ if ("prompts" in body) {
9850
+ const raw2 = body.prompts;
9851
+ if (!Array.isArray(raw2)) errors.push("prompts must be an array");
9852
+ else {
9853
+ if (raw2.length > portal.maxPrompts) errors.push(`prompts supports at most ${portal.maxPrompts} answers`);
9854
+ const seen = /* @__PURE__ */ new Set();
9855
+ const prompts = [];
9856
+ for (const [index2, item] of raw2.entries()) {
9857
+ const entry = item && typeof item === "object" ? item : {};
9858
+ const prompt = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
9859
+ const answer = typeof entry.answer === "string" ? clean(entry.answer) : "";
9860
+ if (!portal.profilePrompts.includes(prompt)) errors.push(`prompts[${index2}].prompt is not one of this site's prompts`);
9861
+ if (seen.has(prompt)) errors.push(`prompts[${index2}].prompt is already answered`);
9862
+ if (entry.answer !== void 0 && typeof entry.answer !== "string") errors.push(`prompts[${index2}].answer must be a string`);
9863
+ if (answer.length > MAX_PROMPT_ANSWER) errors.push(`prompts[${index2}].answer must be at most ${MAX_PROMPT_ANSWER} characters`);
9864
+ seen.add(prompt);
9865
+ prompts.push({ prompt, answer });
9866
+ }
9867
+ patch.prompts = prompts;
9868
+ }
9869
+ }
9870
+ return errors.length ? { ok: false, errors } : { ok: true, patch };
9871
+ }
9872
+ var nameKey = (value) => value.toLowerCase().normalize("NFKC").replace(/[^\p{L}\p{N}\s'-]/gu, " ").replace(/\s+/g, " ").trim();
9873
+
9874
+ // src/member-profile-store.ts
9875
+ async function loadMemberProfile(db, memberId) {
9876
+ return (await db.query({ memberProfiles: { $: { where: { id: memberId }, limit: 1 } } })).memberProfiles?.[0] ?? null;
9877
+ }
9878
+ async function membersNamed(db, mode, chapterId, text11, selfId) {
9879
+ const wanted = nameKey(text11);
9880
+ if (!wanted) return [];
9881
+ 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);
9882
+ return rows6.filter((row2) => String(row2.id) !== selfId && nameKey(`${row2.firstName ?? ""} ${row2.lastName ?? ""}`) === wanted).map((row2) => String(row2.id));
9883
+ }
9884
+ function reviewItemOp(id7, kind, detail, memberId, relatedMemberId) {
9885
+ return { t: "update", ns: "portalReviewItems", id: id7, attrs: {
9886
+ id: id7,
9887
+ kind,
9888
+ detail,
9889
+ status: "open",
9890
+ createdAt: Date.now(),
9891
+ ...memberId ? { memberId } : {},
9892
+ ...relatedMemberId ? { relatedMemberId } : {}
9893
+ } };
9894
+ }
9895
+ async function saveMemberProfile(db, input) {
9896
+ const current = await loadMemberProfile(db, input.memberId);
9897
+ if (Number(current?.revision ?? 0) !== input.expectedRevision) throw new Error("member_profile_revision_conflict");
9898
+ const now = Date.now();
9899
+ const revision = input.expectedRevision + 1;
9900
+ const attrs = { id: input.memberId, memberId: input.memberId, revision, updatedAt: now };
9901
+ const retract = [];
9902
+ const set = (key3, value) => {
9903
+ if (value === null) retract.push(key3);
9904
+ else attrs[key3] = value;
9905
+ };
9906
+ if (input.patch.preferredName !== void 0) set("preferredName", input.patch.preferredName);
9907
+ if (input.patch.location !== void 0) set("location", input.patch.location);
9908
+ if (input.patch.prompts !== void 0) attrs.prompts = input.patch.prompts;
9909
+ const ops = [];
9910
+ let review = null;
9911
+ const introduced = input.patch.introducedByText;
9912
+ if (introduced !== void 0 && introduced !== (current?.introducedByText ?? null)) {
9913
+ set("introducedByText", introduced);
9914
+ if (introduced === null) retract.push("introducedByMemberId", "introducedAt");
9915
+ else {
9916
+ const matches2 = await membersNamed(db, input.mode, input.chapterId, introduced, input.memberId);
9917
+ if (matches2.length === 1) {
9918
+ Object.assign(attrs, { introducedByMemberId: matches2[0], introducedAt: now });
9919
+ review = "matched";
9920
+ } else {
9921
+ retract.push("introducedByMemberId", "introducedAt");
9922
+ ops.push(reviewItemOp(
9923
+ `introduction:${input.memberId}:${revision}`,
9924
+ "introduction_unmatched",
9925
+ { text: introduced, candidates: matches2.length },
9926
+ input.memberId
9927
+ ));
9928
+ review = "queued";
9929
+ }
9930
+ }
9931
+ }
9932
+ ops.unshift({ t: "update", ns: "memberProfiles", id: input.memberId, attrs });
9933
+ const absent = retract.filter((key3) => current && current[key3] !== void 0);
9934
+ if (absent.length) ops.splice(1, 0, { t: "retract", ns: "memberProfiles", id: input.memberId, attrs: absent });
9935
+ await db.transact(ops, {
9936
+ mutationId: `member-profile:${input.memberId}:${revision}`,
9937
+ guards: [current ? { ns: "memberProfiles", id: input.memberId, exists: true, equals: { revision: input.expectedRevision } } : { ns: "memberProfiles", id: input.memberId, exists: false }]
9938
+ });
9939
+ const row2 = { ...current, ...attrs };
9940
+ for (const key3 of absent) delete row2[key3];
9941
+ return { row: row2, review };
9942
+ }
9943
+
9944
+ // src/worker-routes-member-profile.ts
9945
+ var MEMBER_PROFILE_PATH = "/api/member/profile";
9946
+ var MEMBER_PHOTO_PATH = "/api/member/profile/photo";
9947
+ var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
9948
+ var PHOTO_SIZE = 400;
9949
+ async function portalMemberGate(req, env, ctx) {
9950
+ const user = await ctx.verifyUser(req, env);
9951
+ if (!user) return json({ error: "unauthorized" }, 401);
9952
+ const db = ctx.makeDb(env);
9953
+ const resolved = await resolvePortalMember(db, env, ctx, user);
9954
+ if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
9955
+ if (resolved.status !== "member") return json({ error: "member access required" }, 403);
9956
+ return { db, member: resolved.member };
9957
+ }
9958
+ async function profileView(db, ctx, member) {
9959
+ const row2 = await loadMemberProfile(db, member.memberId);
9960
+ const portal = ctx.chapter.memberPortal;
9961
+ return {
9962
+ revision: Number(row2?.revision ?? 0),
9963
+ identity: await portalIdentity(db, ctx, member),
9964
+ profile: {
9965
+ preferredName: row2?.preferredName ?? null,
9966
+ location: row2?.location ?? null,
9967
+ prompts: Array.isArray(row2?.prompts) ? row2.prompts : [],
9968
+ introducedByText: row2?.introducedByText ?? null,
9969
+ introducedByMatched: typeof row2?.introducedByMemberId === "string"
9970
+ },
9971
+ promptChoices: portal.profilePrompts,
9972
+ maxPrompts: portal.maxPrompts,
9973
+ photos: portal.photos
9974
+ };
9975
+ }
9976
+ var handleMemberProfile = async (req, url, env, ctx) => {
9977
+ if (url.pathname !== MEMBER_PROFILE_PATH && url.pathname !== MEMBER_PHOTO_PATH) return null;
9978
+ const portal = ctx.chapter.memberPortal;
9979
+ if (!portal) return json({ error: "not found" }, 404);
9980
+ const gate12 = await portalMemberGate(req, env, ctx);
9981
+ if (gate12 instanceof Response) return gate12;
9982
+ const { db, member } = gate12;
9983
+ if (url.pathname === MEMBER_PHOTO_PATH) return uploadPhoto(req, db, ctx, member);
9984
+ if (req.method === "GET") return json(await profileView(db, ctx, member));
9985
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
9986
+ const body = await readJsonObject(req, 16384);
9987
+ if (body instanceof Response) return body;
9988
+ const expectedRevision = body.expectedRevision;
9989
+ if (!Number.isInteger(expectedRevision) || Number(expectedRevision) < 0) {
9990
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
9991
+ }
9992
+ const checked = validateProfilePatch(body, portal);
9993
+ if (!checked.ok) return json({ error: "invalid profile", errors: checked.errors }, 400);
9994
+ try {
9995
+ const saved = await saveMemberProfile(db, {
9996
+ mode: ctx.chapter.mode,
9997
+ chapterId: ctx.chapter.id,
9998
+ memberId: member.memberId,
9999
+ patch: checked.patch,
10000
+ expectedRevision: Number(expectedRevision)
10001
+ });
10002
+ return json({ ...await profileView(db, ctx, member), introducedBy: saved.review });
10003
+ } catch (error) {
10004
+ if (error instanceof Error && error.message === "member_profile_revision_conflict") {
10005
+ return json({ error: "your profile changed elsewhere; reload before saving" }, 409);
10006
+ }
10007
+ return json({ error: "profile could not be saved" }, 503);
10008
+ }
10009
+ };
10010
+ async function uploadPhoto(req, db, ctx, member) {
10011
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
10012
+ if (!ctx.chapter.memberPortal?.photos || !db.storage) return json({ error: "photos are not enabled" }, 404);
10013
+ const declared = Number(req.headers.get("content-length") ?? 0);
10014
+ if (declared > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
10015
+ const bytes = new Uint8Array(await req.arrayBuffer());
10016
+ if (bytes.length > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
10017
+ const image = cleanImage(bytes);
10018
+ if (!image) return json({ error: "photo must be a JPEG or PNG" }, 415);
10019
+ if (image.width > PHOTO_SIZE || image.height > PHOTO_SIZE) {
10020
+ return json({ error: `photo must be at most ${PHOTO_SIZE} by ${PHOTO_SIZE} pixels` }, 422);
10021
+ }
10022
+ const now = Date.now();
10023
+ const path = `member-photos/${encodeURIComponent(member.memberId)}/${now}.${image.extension}`;
10024
+ await db.storage.upload(path, new Blob([image.bytes], { type: image.contentType }), image.contentType, { private: true });
10025
+ const previous = await loadMemberProfile(db, member.memberId);
10026
+ await db.transact([{ t: "update", ns: "memberProfiles", id: member.memberId, attrs: {
10027
+ id: member.memberId,
10028
+ memberId: member.memberId,
10029
+ photoPath: path,
10030
+ photoUpdatedAt: now,
10031
+ updatedAt: now,
10032
+ ...previous ? {} : { revision: 0 }
10033
+ } }], { mutationId: `member-photo:${member.memberId}:${now}` });
10034
+ const old = typeof previous?.photoPath === "string" ? previous.photoPath : null;
10035
+ if (old && old !== path) await db.storage.delete(old).catch(() => void 0);
10036
+ return json({ photoUrl: await db.storage.sign(path, 3600), width: image.width, height: image.height });
10037
+ }
10038
+
10039
+ // src/worker-routes-admin-member-tier.ts
10040
+ var PATH5 = /^\/api\/admin\/applications\/([^/]+)\/entitlement-tier$/;
10041
+ var handleAdminMemberTier = async (req, url, env, ctx) => {
10042
+ const match = PATH5.exec(url.pathname);
10043
+ if (!match) return null;
10044
+ if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
10045
+ if (!["GET", "POST", "DELETE"].includes(req.method)) return json({ error: "method not allowed" }, 405);
10046
+ const user = await ctx.verifyUser(req, env);
10047
+ if (!user) return json({ error: "unauthorized" }, 401);
10048
+ const db = ctx.makeDb(env);
10049
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
10050
+ const id7 = decodeURIComponent(match[1]);
10051
+ const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
10052
+ if (!app || app.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
10053
+ const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
10054
+ const tiers = group ? await loadTiers(db, group) : [];
10055
+ if (req.method === "GET") {
10056
+ const history = (await db.query({ memberTierChanges: { $: { where: { applicationId: id7 }, limit: 50 } } })).memberTierChanges ?? [];
10057
+ return json({
10058
+ purchasedTierId: app.tierId ?? null,
10059
+ entitledTierId: entitledTierId(app),
10060
+ entitlementTierEndsAt: app.entitlementTierEndsAt ?? null,
10061
+ tiers: tiers.filter((tier) => tier.active !== false).map(({ id: tierId, name: name2 }) => ({ id: tierId, name: name2 })),
10062
+ history: history.sort((a, b) => Number(b.at) - Number(a.at))
10063
+ });
10064
+ }
10065
+ const body = await readJsonObject(req, 8192);
10066
+ if (body instanceof Response) return body;
10067
+ if (app.status !== "approved" || app.canceled === true) return json({ error: "only an approved member can change tier" }, 409);
10068
+ const reason = typeof body.reason === "string" ? body.reason.trim() : "";
10069
+ if (!reason || reason.length > 500) return json({ error: "a reason of up to 500 characters is required" }, 400);
10070
+ const paymentReference = typeof body.paymentReference === "string" && body.paymentReference.trim() ? body.paymentReference.trim().slice(0, 200) : void 0;
10071
+ const now = Date.now();
10072
+ let toTierId;
10073
+ let endsAt;
10074
+ if (req.method === "POST") {
10075
+ toTierId = typeof body.tierId === "string" ? body.tierId : "";
10076
+ const tier = tiers.find((candidate) => candidate.id === toTierId);
10077
+ if (!tier || tier.active === false) return json({ error: "tierId must be an active tier" }, 400);
10078
+ if (body.endsAt !== void 0) {
10079
+ endsAt = Number(body.endsAt);
10080
+ if (!Number.isSafeInteger(endsAt) || endsAt <= now) return json({ error: "endsAt must be a future timestamp" }, 400);
10081
+ }
10082
+ } else if (app.entitlementTierId === void 0) return json({ error: "no entitlement tier to revert" }, 409);
10083
+ const fromTierId = entitledTierId(app, now) ?? void 0;
10084
+ const changeId = `tier-change:${id7}:${now}`;
10085
+ const ops = [
10086
+ { t: "update", ns: "memberTierChanges", id: changeId, attrs: {
10087
+ id: changeId,
10088
+ applicationId: id7,
10089
+ reason,
10090
+ actor: user.email ?? user.userId,
10091
+ at: now,
10092
+ ...fromTierId ? { fromTierId } : {},
10093
+ ...toTierId ? { toTierId } : {},
10094
+ ...endsAt ? { endsAt } : {},
10095
+ ...paymentReference ? { paymentReference } : {}
10096
+ } },
10097
+ toTierId ? { t: "update", ns: "applications", id: id7, attrs: { entitlementTierId: toTierId, ...endsAt ? { entitlementTierEndsAt: endsAt } : {} } } : { t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierId", "entitlementTierEndsAt"] }
10098
+ ];
10099
+ if (toTierId && !endsAt && app.entitlementTierEndsAt !== void 0) ops.push({ t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierEndsAt"] });
10100
+ const current = app.entitlementTierId;
10101
+ try {
10102
+ 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 } }] });
10103
+ } catch {
10104
+ return json({ error: "this member changed; reload and try again" }, 409);
10105
+ }
10106
+ return json({ ok: true, applicationId: id7, fromTierId: fromTierId ?? null, toTierId: toTierId ?? (typeof app.tierId === "string" ? app.tierId : null), endsAt: endsAt ?? null });
10107
+ };
10108
+
10109
+ // src/worker-routes-admin-portal-review.ts
10110
+ var PORTAL_REVIEW_PATH = "/api/admin/portal-review";
10111
+ var handleAdminPortalReview = async (req, url, env, ctx) => {
10112
+ if (url.pathname !== PORTAL_REVIEW_PATH && !url.pathname.startsWith(`${PORTAL_REVIEW_PATH}/`)) return null;
10113
+ if (!ctx.chapter.memberPortal) return json({ error: "not found" }, 404);
10114
+ const user = await ctx.verifyUser(req, env);
10115
+ if (!user) return json({ error: "unauthorized" }, 401);
10116
+ const db = ctx.makeDb(env);
10117
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
10118
+ if (req.method === "GET" && url.pathname === PORTAL_REVIEW_PATH) {
10119
+ const status2 = url.searchParams.get("status") ?? "open";
10120
+ const where = status2 === "all" ? {} : { status: status2 };
10121
+ const items = (await db.query({ portalReviewItems: { $: { where, limit: 1e3 } } })).portalReviewItems ?? [];
10122
+ return json({ items: items.sort((a, b) => Number(b.createdAt) - Number(a.createdAt)) });
10123
+ }
10124
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
10125
+ const id7 = decodeURIComponent(url.pathname.slice(PORTAL_REVIEW_PATH.length + 1));
10126
+ const body = await readJsonObject(req, 4096);
10127
+ if (body instanceof Response) return body;
10128
+ const decision = typeof body.decision === "string" ? body.decision.trim() : "";
10129
+ if (!decision || decision.length > 1e3) return json({ error: "a decision of up to 1,000 characters is required" }, 400);
10130
+ const item = (await db.query({ portalReviewItems: { $: { where: { id: id7 }, limit: 1 } } })).portalReviewItems?.[0];
10131
+ if (!item) return json({ error: "not found" }, 404);
10132
+ if (item.status !== "open") return json({ error: "already resolved" }, 409);
10133
+ const attrs = { status: "resolved", decision, resolvedBy: user.email ?? user.userId, resolvedAt: Date.now() };
10134
+ try {
10135
+ await db.transact([{ t: "update", ns: "portalReviewItems", id: id7, attrs }], {
10136
+ mutationId: `portal-review:${id7}:resolve`,
10137
+ guards: [{ ns: "portalReviewItems", id: id7, exists: true, equals: { status: "open" } }]
10138
+ });
10139
+ } catch {
10140
+ return json({ error: "already resolved" }, 409);
10141
+ }
10142
+ return json({ item: { ...item, ...attrs } });
10143
+ };
10144
+
10145
+ // src/worker-routes-member-portal.ts
10146
+ var PORTAL_ROUTES = [
10147
+ handleMemberProfile,
10148
+ handleAdminHubMembers,
10149
+ handleAdminMemberTier,
10150
+ handleAdminPortalReview
10151
+ ];
10152
+ var handleMemberPortal = async (req, url, env, ctx) => {
10153
+ for (const route of PORTAL_ROUTES) {
10154
+ const response = await route(req, url, env, ctx);
10155
+ if (response) return response;
10156
+ }
10157
+ return null;
10158
+ };
10159
+
9006
10160
  // src/worker-routes-admission-grants.ts
9007
10161
  import { verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
9008
10162
  var reject5 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
@@ -9383,9 +10537,9 @@ function integer3(value, fallback, min, max) {
9383
10537
  const parsed = Number(value);
9384
10538
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
9385
10539
  }
9386
- function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
10540
+ function projectRecord(record4, allowed, editable, stagesEnabled, crm) {
9387
10541
  const fields = Object.fromEntries(
9388
- allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
10542
+ allowed.flatMap((name2) => Object.prototype.hasOwnProperty.call(record4.fields, name2) ? [[name2, record4.fields[name2]]] : [])
9389
10543
  );
9390
10544
  const def = crm.type(record4.type);
9391
10545
  const editFields = editable.map((id7) => {
@@ -9495,7 +10649,7 @@ async function deliverAdmissionGrants(options) {
9495
10649
  }
9496
10650
 
9497
10651
  // src/worker-routes-network-admission.ts
9498
- var PATH5 = "/api/admin/network/admissions";
10652
+ var PATH6 = "/api/admin/network/admissions";
9499
10653
  var RETRY_PATH = "/api/admin/network/admissions/retry";
9500
10654
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
9501
10655
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -9575,11 +10729,11 @@ async function parseBody(req) {
9575
10729
  }
9576
10730
  }
9577
10731
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
9578
- if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
10732
+ if (![PATH6, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
9579
10733
  const auth = await gate2(req, env, ctx);
9580
10734
  if ("response" in auth) return auth.response;
9581
10735
  const db = auth.db;
9582
- if (url.pathname === PATH5 && req.method === "GET") {
10736
+ if (url.pathname === PATH6 && req.method === "GET") {
9583
10737
  const targetId2 = url.searchParams.get("targetId") ?? "";
9584
10738
  const target2 = targetFor(ctx, targetId2);
9585
10739
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -9727,8 +10881,8 @@ function makeCalendar(env) {
9727
10881
  }
9728
10882
  async function firstRow3(db, ns, q) {
9729
10883
  const res = await db.query({ [ns]: { $: q } });
9730
- const rows5 = res[ns];
9731
- return Array.isArray(rows5) ? rows5[0] : void 0;
10884
+ const rows6 = res[ns];
10885
+ return Array.isArray(rows6) ? rows6[0] : void 0;
9732
10886
  }
9733
10887
  async function computeSlots(cal, cfg) {
9734
10888
  const { from, to } = slotWindow(Date.now(), cfg.windowDays);
@@ -10587,18 +11741,18 @@ async function startSubscription(req, env, ctx) {
10587
11741
 
10588
11742
  // src/worker-payment-receipt.ts
10589
11743
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10590
- var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
11744
+ var escape = (text11) => text11.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10591
11745
  async function paymentReceiptRoute(req, url, env, ctx) {
10592
11746
  const id7 = url.searchParams.get("application");
10593
11747
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
10594
11748
  if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
10595
11749
  const db = ctx.makeDb(env);
10596
- const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
10597
- const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
11750
+ const rows6 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
11751
+ const app = rows6.applications?.[0], intent = rows6.subscriptionCheckoutIntents?.[0];
10598
11752
  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);
10599
11753
  const receipt = intent.paymentReceipt;
10600
11754
  if (!receipt) return json2({ pending: true }, 202);
10601
- const text10 = paymentReceiptText(receipt, ctx.chapter.name);
11755
+ const text11 = paymentReceiptText(receipt, ctx.chapter.name);
10602
11756
  if (req.method === "POST") {
10603
11757
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10604
11758
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10616,7 +11770,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10616
11770
  template: "paymentReceipt",
10617
11771
  to: String(intent.purchaserEmailNormalized),
10618
11772
  applicationId: id7,
10619
- vars: { chapterName: ctx.chapter.name, receiptText: text10 },
11773
+ vars: { chapterName: ctx.chapter.name, receiptText: text11 },
10620
11774
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10621
11775
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10622
11776
  });
@@ -10626,7 +11780,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10626
11780
  const nonce = crypto.randomUUID();
10627
11781
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10628
11782
  <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>
10629
- <h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
11783
+ <h1>Payment receipt</h1><pre>${escape(text11)}</pre><button id="print">Print / save PDF</button>
10630
11784
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10631
11785
  headers: {
10632
11786
  "content-type": "text/html; charset=utf-8",
@@ -10659,7 +11813,7 @@ var handlePayments = async (req, url, env, ctx) => {
10659
11813
 
10660
11814
  // src/worker-routes-gifts.ts
10661
11815
  var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
10662
- var rows3 = (value) => Array.isArray(value) ? value : [];
11816
+ var rows4 = (value) => Array.isArray(value) ? value : [];
10663
11817
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
10664
11818
  var userFor = async (req, env, ctx) => {
10665
11819
  const user = await ctx.verifyUser(req, env);
@@ -10697,7 +11851,7 @@ var handleGifts = async (req, url, env, ctx) => {
10697
11851
  const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
10698
11852
  return json({
10699
11853
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
10700
- claims: buyerGiftClaims(rows3(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
11854
+ claims: buyerGiftClaims(rows4(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
10701
11855
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
10702
11856
  merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
10703
11857
  });
@@ -10720,7 +11874,7 @@ var handleGifts = async (req, url, env, ctx) => {
10720
11874
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
10721
11875
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
10722
11876
  const emailNormalized = normalizeChapterEmail(user.email);
10723
- const applications = rows3((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
11877
+ const applications = rows4((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
10724
11878
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
10725
11879
  try {
10726
11880
  return json(await startGiftIntent(db, {
@@ -10850,9 +12004,9 @@ var receiptFor2 = (seat, released) => ({
10850
12004
  });
10851
12005
  async function regiftNamedSeat(db, input, now) {
10852
12006
  membershipClock(now);
10853
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
12007
+ const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
10854
12008
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
10855
- 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");
12009
+ 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");
10856
12010
  const state = await loadMembershipAuthorityState(db, input);
10857
12011
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
10858
12012
  requireMembershipScope(seat, input);
@@ -10861,7 +12015,7 @@ async function regiftNamedSeat(db, input, now) {
10861
12015
  if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
10862
12016
  if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
10863
12017
  const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
10864
- if (seat.recipientEmailNormalized === email && seat.recipientName === name && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
12018
+ if (seat.recipientEmailNormalized === email && seat.recipientName === name2 && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
10865
12019
  return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
10866
12020
  }
10867
12021
  if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
@@ -10899,7 +12053,7 @@ async function regiftNamedSeat(db, input, now) {
10899
12053
  });
10900
12054
  const ops = [
10901
12055
  membershipUpdate("networkSeat", String(seat.id), {
10902
- recipientName: name,
12056
+ recipientName: name2,
10903
12057
  recipientEmailNormalized: email,
10904
12058
  recipientClaimId: claimId,
10905
12059
  status: "pending_acceptance",
@@ -11096,16 +12250,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
11096
12250
  };
11097
12251
 
11098
12252
  // src/named-seat-accepted-notice.ts
11099
- var text7 = (value) => typeof value === "string" ? value : "";
12253
+ var text8 = (value) => typeof value === "string" ? value : "";
11100
12254
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
11101
12255
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
11102
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
12256
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text8(purchaser.email)) {
11103
12257
  return { sent: false, reason: "purchaser-missing" };
11104
12258
  }
11105
12259
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
11106
12260
  if (!group) return { sent: false, reason: "group-missing" };
11107
12261
  const config = emailGroupFrom(group);
11108
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
12262
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text8).join(" ").replace(/\s+/g, " ").trim();
11109
12263
  return sendTemplated({
11110
12264
  db,
11111
12265
  envName: env.ODLA_ENV,
@@ -11117,12 +12271,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
11117
12271
  }, {
11118
12272
  group: config,
11119
12273
  template: "seatAccepted",
11120
- to: text7(purchaser.email),
12274
+ to: text8(purchaser.email),
11121
12275
  dedupeKey: `named-seat-accepted:${input.seatId}`,
11122
12276
  applicationId: input.primaryApplicationId,
11123
12277
  tierId: input.skuId,
11124
12278
  vars: {
11125
- firstName: text7(purchaser.firstName),
12279
+ firstName: text8(purchaser.firstName),
11126
12280
  purchaserName,
11127
12281
  recipientName: input.recipientName,
11128
12282
  tierName: await tierName(db, group, input.skuId),
@@ -11583,8 +12737,8 @@ async function upcomingEvents(env) {
11583
12737
  const res = await cal.availability.upcoming();
11584
12738
  return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
11585
12739
  }
11586
- function toReconcile(rows5) {
11587
- return rows5.map((m) => ({
12740
+ function toReconcile(rows6) {
12741
+ return rows6.map((m) => ({
11588
12742
  id: String(m.id),
11589
12743
  applicationId: String(m.applicationId),
11590
12744
  googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
@@ -11606,12 +12760,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11606
12760
  const query = await db.query({
11607
12761
  meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
11608
12762
  });
11609
- let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
11610
- if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
11611
- if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
12763
+ let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
12764
+ if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
12765
+ if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
11612
12766
  let decisions = [];
11613
12767
  try {
11614
- decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
12768
+ decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
11615
12769
  } catch {
11616
12770
  }
11617
12771
  const patched = /* @__PURE__ */ new Map();
@@ -11638,7 +12792,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11638
12792
  };
11639
12793
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
11640
12794
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
11641
- const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
12795
+ const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
11642
12796
  return json({ meetings, adopted: decisions.length, timezone });
11643
12797
  };
11644
12798
 
@@ -11777,15 +12931,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11777
12931
  for (const a of data.applications ?? []) {
11778
12932
  const key3 = String(a.email ?? "").toLowerCase();
11779
12933
  if (!key3) continue;
11780
- const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
12934
+ const name2 = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
11781
12935
  const row2 = people.get(key3);
11782
12936
  if (row2) {
11783
12937
  if (!row2.application) row2.application = applicationSummary(a);
11784
- if (!row2.name) row2.name = name;
12938
+ if (!row2.name) row2.name = name2;
11785
12939
  } else {
11786
12940
  people.set(key3, {
11787
12941
  email: String(a.email),
11788
- name,
12942
+ name: name2,
11789
12943
  userId: null,
11790
12944
  role: null,
11791
12945
  profile: null,
@@ -11794,10 +12948,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11794
12948
  });
11795
12949
  }
11796
12950
  }
11797
- const rows5 = [...people.values()].sort(
12951
+ const rows6 = [...people.values()].sort(
11798
12952
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
11799
12953
  );
11800
- return json({ people: rows5 });
12954
+ return json({ people: rows6 });
11801
12955
  };
11802
12956
  var handleAdminPeopleAccess = async (req, url, env, ctx) => {
11803
12957
  if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
@@ -11998,7 +13152,7 @@ async function gate3(req, env, ctx) {
11998
13152
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
11999
13153
  return rawDb;
12000
13154
  }
12001
- var rows4 = (v) => Array.isArray(v) ? v : [];
13155
+ var rows5 = (v) => Array.isArray(v) ? v : [];
12002
13156
  var handleAdminDashboard = async (req, url, env, ctx) => {
12003
13157
  if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
12004
13158
  const got = await gate3(req, env, ctx);
@@ -12020,8 +13174,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12020
13174
  crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
12021
13175
  groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
12022
13176
  });
12023
- const apps = rows4(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
12024
- const incomplete = rows4(data.applications).length >= 1e3 || rows4(data.crm_record).length >= 1e3 || rows4(data.meetings).length >= 500;
13177
+ const apps = rows5(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
13178
+ const incomplete = rows5(data.applications).length >= 1e3 || rows5(data.crm_record).length >= 1e3 || rows5(data.meetings).length >= 500;
12025
13179
  const applications = {
12026
13180
  total: apps.length,
12027
13181
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -12037,7 +13191,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12037
13191
  for (const a of apps) {
12038
13192
  if (String(a.status) in pipeline) pipeline[String(a.status)] = (pipeline[String(a.status)] ?? 0) + 1;
12039
13193
  }
12040
- for (const r of rows4(data.crm_record)) {
13194
+ for (const r of rows5(data.crm_record)) {
12041
13195
  if (!appById.has(r.fields?.applicationId)) continue;
12042
13196
  const s = r.stage;
12043
13197
  if (s in pipeline) {
@@ -12045,7 +13199,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12045
13199
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
12046
13200
  }
12047
13201
  }
12048
- const meetings = rows4(data.meetings).filter((m) => appById.has(m.applicationId));
13202
+ const meetings = rows5(data.meetings).filter((m) => appById.has(m.applicationId));
12049
13203
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
12050
13204
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
12051
13205
  const agenda = upcoming.slice(0, 8).map((m) => {
@@ -12068,12 +13222,12 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12068
13222
  const sk = await getVaultSecret(db, "stripe_secret_key");
12069
13223
  if (sk) {
12070
13224
  const subsRes = await chapterBillingSubscriptions(sk, ctx.chapter.id, env).catch(() => null);
12071
- if (subsRes?.complete && rows4(data.applications).length < 1e3) {
13225
+ if (subsRes?.complete && rows5(data.applications).length < 1e3) {
12072
13226
  const subs = subsRes.objects.filter((s) => {
12073
13227
  const a = appById.get(s.metadata?.applicationId);
12074
13228
  return a?.stripeSubscriptionId === s.id && a?.stripeCustomerId === s.customer;
12075
13229
  });
12076
- const intents = new Map(rows4(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
13230
+ const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
12077
13231
  const amount = (s) => chapterBillingAmount(s, intents.get(s.metadata?.applicationId), env);
12078
13232
  const subMs = (s) => (s.created ?? 0) * 1e3;
12079
13233
  membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
@@ -12112,9 +13266,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12112
13266
  const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
12113
13267
  if (!subsRes) return json({ error: "billing lookup failed upstream" }, 502);
12114
13268
  const subById = new Map(subsRes.objects.map((s) => [s.id, s]));
12115
- const intents = new Map(rows4(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
13269
+ const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
12116
13270
  const billingRows = [];
12117
- for (const a of rows4(data.applications)) {
13271
+ for (const a of rows5(data.applications)) {
12118
13272
  if (!chapterBillingApplication(a, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
12119
13273
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
12120
13274
  const candidate = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
@@ -12150,9 +13304,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12150
13304
  return json({
12151
13305
  billingReady: true,
12152
13306
  testMode,
12153
- truncated: !subsRes.complete || rows4(data.applications).length >= 200,
13307
+ truncated: !subsRes.complete || rows5(data.applications).length >= 200,
12154
13308
  rows: billingRows,
12155
- summary: subsRes.complete && rows4(data.applications).length < 200 ? summary : null
13309
+ summary: subsRes.complete && rows5(data.applications).length < 200 ? summary : null
12156
13310
  });
12157
13311
  };
12158
13312
 
@@ -12508,7 +13662,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
12508
13662
  import { getConfig, saveConfig, validateEmailCopy as validateEmailCopy4 } from "@odla-ai/crm";
12509
13663
 
12510
13664
  // src/email-readiness.ts
12511
- import { isEmailTierName as isEmailTierName2, validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
13665
+ import { isEmailTierName as isEmailTierName2, templateSectionNames as templateSectionNames2, templateVars as templateVars2, validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
12512
13666
 
12513
13667
  // src/email-spam-signals.ts
12514
13668
  var TRIGGER_PHRASES = [
@@ -12544,7 +13698,8 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
12544
13698
  var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
12545
13699
  function emailSpamSignals(copy) {
12546
13700
  const subject = typeof copy.subject === "string" ? copy.subject : "";
12547
- const text10 = typeof copy.text === "string" ? copy.text : "";
13701
+ const text11 = typeof copy.text === "string" ? copy.text : "";
13702
+ const html = typeof copy.html === "string" ? copy.html : "";
12548
13703
  const out = [];
12549
13704
  const letters = strip(subject).replace(/[^A-Za-z]/g, "");
12550
13705
  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, "")));
@@ -12553,11 +13708,12 @@ function emailSpamSignals(copy) {
12553
13708
  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." });
12554
13709
  const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
12555
13710
  if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
12556
- const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
13711
+ const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text11));
12557
13712
  if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
12558
- 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." });
12559
- if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
12560
- const urls = [...text10.matchAll(URL_RE2)].map((match) => match[0]);
13713
+ 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." });
13714
+ if (/!{2,}/.test(text11)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
13715
+ const urls = [...`${text11}
13716
+ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
12561
13717
  const hosts = urls.map((url) => {
12562
13718
  try {
12563
13719
  return new URL(url).hostname;
@@ -12569,7 +13725,7 @@ function emailSpamSignals(copy) {
12569
13725
  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." });
12570
13726
  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." });
12571
13727
  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." });
12572
- 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." });
13728
+ 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." });
12573
13729
  return out;
12574
13730
  }
12575
13731
  function emailSenderWarnings(input) {
@@ -12602,11 +13758,15 @@ function validateEmailReadiness(input) {
12602
13758
  const template = input.templates[key3];
12603
13759
  const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy3(template, { allowedVariables: event2.variables });
12604
13760
  if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
13761
+ const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
13762
+ if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
12605
13763
  if (template) {
12606
- const used = [...`${template.subject ?? ""}
12607
- ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
12608
- for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
12609
- problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
13764
+ const copy = `${template.subject ?? ""}
13765
+ ${template.text ?? ""}
13766
+ ${html}`;
13767
+ const sectionOnly = new Set(templateSectionNames2(copy));
13768
+ for (const name2 of templateVars2(copy)) if (name2 in input.groupValues && !input.groupValues[name2]?.trim() && !sectionOnly.has(name2)) {
13769
+ problems.push({ code: "value-missing", variable: name2, message: `Add the ${name2} used by this email.` });
12610
13770
  }
12611
13771
  }
12612
13772
  if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
@@ -12634,6 +13794,7 @@ async function emailSettings(db, ctx, env) {
12634
13794
  const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
12635
13795
  subject: str2(stored[key3]?.subject),
12636
13796
  text: str2(stored[key3]?.text),
13797
+ html: str2(stored[key3]?.html),
12637
13798
  enabled: stored[key3]?.enabled
12638
13799
  }]));
12639
13800
  let tiers = [], tiersUnavailable = false;
@@ -12674,18 +13835,20 @@ async function updateEmailSettings(db, ctx, env, body) {
12674
13835
  if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
12675
13836
  const templates = body.emailTemplates;
12676
13837
  if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
12677
- const clean = {};
13838
+ const clean2 = {};
12678
13839
  for (const [key3, value] of Object.entries(templates)) {
12679
13840
  if (!events[key3]) continue;
12680
13841
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12681
13842
  const t = value;
12682
13843
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12683
- const subject = str2(t.subject).trim(), text10 = str2(t.text);
12684
- const issues = validateEmailCopy4({ subject, text: text10 }, { allowedVariables: events[key3].variables });
12685
- if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
12686
- return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
13844
+ const subject = str2(t.subject).trim(), text11 = str2(t.text), html = str2(t.html);
13845
+ if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
13846
+ const issues = validateEmailCopy4({ subject, text: text11 }, { allowedVariables: events[key3].variables });
13847
+ const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
13848
+ if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
13849
+ return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
12687
13850
  }
12688
- clean[key3] = { subject, text: text10, enabled: t.enabled };
13851
+ clean2[key3] = { subject, text: text11, ...html.trim() ? { html } : {}, enabled: t.enabled };
12689
13852
  }
12690
13853
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12691
13854
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -12698,15 +13861,15 @@ async function updateEmailSettings(db, ctx, env, body) {
12698
13861
  if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
12699
13862
  if (ctx.chapter.mode === "hub") {
12700
13863
  const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
12701
- await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
13864
+ await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean2 }, notificationEmail, replyTo, debugEmail });
12702
13865
  } else {
12703
13866
  const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
12704
13867
  await db.transact([
12705
- { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
13868
+ { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean2 } },
12706
13869
  { t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
12707
13870
  ]);
12708
13871
  }
12709
- const warnings = Object.fromEntries(Object.entries(clean).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
13872
+ const warnings = Object.fromEntries(Object.entries(clean2).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
12710
13873
  return json({ ok: true, warnings });
12711
13874
  }
12712
13875
 
@@ -12744,8 +13907,8 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
12744
13907
  const got = await gate5(req, env, ctx);
12745
13908
  if (got instanceof Response) return got;
12746
13909
  if (ctx.chapter.mode === "hub") {
12747
- const rows5 = await listEmailLog({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
12748
- return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
13910
+ const rows6 = await listEmailLog({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
13911
+ return json({ sends: rows6.map((row2) => ({ ...row2, template: row2.templateId })) });
12749
13912
  }
12750
13913
  const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
12751
13914
  const visible = [];
@@ -12975,7 +14138,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
12975
14138
  return json({
12976
14139
  targets: ctx.chapter.network.targets.map(({
12977
14140
  id: id7,
12978
- name,
14141
+ name: name2,
12979
14142
  url: targetUrl,
12980
14143
  fields,
12981
14144
  sharedNotes,
@@ -12984,7 +14147,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
12984
14147
  commercialParity
12985
14148
  }) => ({
12986
14149
  id: id7,
12987
- name,
14150
+ name: name2,
12988
14151
  url: targetUrl,
12989
14152
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
12990
14153
  sharedNotes,
@@ -13323,8 +14486,8 @@ function terms(value) {
13323
14486
  function matchesReaderIdentity(record4, search, includeEmail) {
13324
14487
  const query = terms(search);
13325
14488
  if (!query.length) return true;
13326
- const name = folded(record4.name);
13327
- const visible = [name, ...name.split(/\s+/u).filter(Boolean)];
14489
+ const name2 = folded(record4.name);
14490
+ const visible = [name2, ...name2.split(/\s+/u).filter(Boolean)];
13328
14491
  if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
13329
14492
  return query.every((term) => visible.some((candidate) => candidate.includes(term)));
13330
14493
  }
@@ -13353,14 +14516,14 @@ async function listVisibleSearchPopulation(db, where, window, count) {
13353
14516
  })));
13354
14517
  return pages.flatMap((page) => (page[CRM_NS.record] ?? []).map(rowToRecord));
13355
14518
  }
13356
- async function listReaderRecords(db, type, allowed2, emailField, window) {
14519
+ async function listReaderRecords(db, type, allowed, emailField, window) {
13357
14520
  const where = baseWhere(type, window.stage);
13358
14521
  const crmDb = db;
13359
14522
  const aggregate = await crmDb.aggregate(CRM_NS.record, { count: true }, { where });
13360
14523
  const candidateCount = aggregate.count ?? 0;
13361
14524
  if (window.search) {
13362
14525
  const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
13363
- const includeEmail = Boolean(emailField && allowed2.includes(emailField));
14526
+ const includeEmail = Boolean(emailField && allowed.includes(emailField));
13364
14527
  const matches2 = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
13365
14528
  return {
13366
14529
  records: matches2.slice(window.offset, window.offset + window.limit),
@@ -13453,7 +14616,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13453
14616
  const editable = reader.editableFields[input.type];
13454
14617
  if (hasFields) {
13455
14618
  const names = Object.keys(fields);
13456
- if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
14619
+ if (!editable || names.length === 0 || names.length > 32 || names.some((name2) => !editable.includes(name2))) {
13457
14620
  return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
13458
14621
  }
13459
14622
  } else if (!reader.stageTransitions.includes(input.type)) {
@@ -13518,7 +14681,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13518
14681
  });
13519
14682
  }
13520
14683
  const capabilities = await operationalReaderRecord(record4, reader, db, env, ctx);
13521
- if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
14684
+ if (hasFields && Object.keys(fields).some((name2) => !capabilities.edit?.fields.some((field) => field.id === name2))) {
13522
14685
  return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
13523
14686
  }
13524
14687
  const deps = { crm: ctx.chapter.crm, db };
@@ -13573,8 +14736,8 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13573
14736
  }
13574
14737
  }
13575
14738
  const type = url.searchParams.get("type") ?? "";
13576
- const allowed2 = reader.fields[type];
13577
- if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
14739
+ const allowed = reader.fields[type];
14740
+ if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
13578
14741
  const recordId = url.searchParams.get("recordId");
13579
14742
  if (recordId !== null) {
13580
14743
  if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
@@ -13602,7 +14765,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13602
14765
  page = await listReaderRecords(
13603
14766
  db,
13604
14767
  type,
13605
- allowed2,
14768
+ allowed,
13606
14769
  typeDef.emailField,
13607
14770
  window
13608
14771
  );
@@ -13683,8 +14846,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
13683
14846
  async function mutateTargetRecord(db, env, ctx, target, input) {
13684
14847
  const fields = input.fields;
13685
14848
  if (fields) {
13686
- const allowed2 = target.editableFields[input.type];
13687
- if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
14849
+ const allowed = target.editableFields[input.type];
14850
+ if (!allowed || Object.keys(fields).some((name2) => !allowed.includes(name2))) {
13688
14851
  return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
13689
14852
  }
13690
14853
  } else if (!target.stageTransitions.includes(input.type)) {
@@ -14532,20 +15695,20 @@ async function networkPersonBulkSha256(value) {
14532
15695
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
14533
15696
  return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
14534
15697
  }
14535
- function assertNetworkPersonBulkRows(rows5) {
14536
- if (!Array.isArray(rows5) || rows5.length < 1) {
15698
+ function assertNetworkPersonBulkRows(rows6) {
15699
+ if (!Array.isArray(rows6) || rows6.length < 1) {
14537
15700
  throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
14538
15701
  }
14539
- if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
15702
+ if (rows6.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
14540
15703
  throw new NetworkPersonRegistryError(
14541
15704
  "bulk_rows_exceeded",
14542
15705
  `bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
14543
15706
  );
14544
15707
  }
14545
- return rows5;
15708
+ return rows6;
14546
15709
  }
14547
- async function networkPersonBulkBatchDigest(rows5) {
14548
- const bounded = assertNetworkPersonBulkRows(rows5);
15710
+ async function networkPersonBulkBatchDigest(rows6) {
15711
+ const bounded = assertNetworkPersonBulkRows(rows6);
14549
15712
  return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
14550
15713
  }
14551
15714
  async function networkPersonRegistryStateDigest(state) {
@@ -14560,7 +15723,7 @@ async function networkPersonRegistryStateDigest(state) {
14560
15723
  function rejectedValidation(errors) {
14561
15724
  return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
14562
15725
  }
14563
- async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
15726
+ async function classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state) {
14564
15727
  const aliasIds = new Set(state.aliases.map((alias) => alias.id));
14565
15728
  const batchAliases = /* @__PURE__ */ new Set();
14566
15729
  const claimOwners = /* @__PURE__ */ new Map();
@@ -14571,8 +15734,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14571
15734
  }
14572
15735
  const results = [];
14573
15736
  const values = [];
14574
- for (let index2 = 0; index2 < rows5.length; index2 += 1) {
14575
- const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
15737
+ for (let index2 = 0; index2 < rows6.length; index2 += 1) {
15738
+ const validation = validateNetworkPersonImport(rows6[index2], allowedChapterIds);
14576
15739
  if (!validation.ok || !validation.value) {
14577
15740
  const errors = validation.errors.slice(0, 8);
14578
15741
  results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
@@ -14605,8 +15768,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14605
15768
  }
14606
15769
  return { rows: results, values };
14607
15770
  }
14608
- async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
14609
- const bounded = assertNetworkPersonBulkRows(rows5);
15771
+ async function previewNetworkPersonBulkImport(db, rows6, allowedChapterIds) {
15772
+ const bounded = assertNetworkPersonBulkRows(rows6);
14610
15773
  const state = await loadNetworkPersonRegistry(db);
14611
15774
  const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
14612
15775
  const counts = networkPersonBulkEmptyCounts();
@@ -14637,8 +15800,8 @@ function eventCounts(event2) {
14637
15800
  }
14638
15801
  async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
14639
15802
  if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
14640
- const rows5 = assertNetworkPersonBulkRows(input.rows);
14641
- const batchDigest = await networkPersonBulkBatchDigest(rows5);
15803
+ const rows6 = assertNetworkPersonBulkRows(input.rows);
15804
+ const batchDigest = await networkPersonBulkBatchDigest(rows6);
14642
15805
  if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
14643
15806
  throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
14644
15807
  }
@@ -14657,7 +15820,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14657
15820
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14658
15821
  batchDigest,
14659
15822
  stateDigest: eventString(completed, "stateDigest"),
14660
- rowCount: Number(completed.detail.rowCount) || rows5.length,
15823
+ rowCount: Number(completed.detail.rowCount) || rows6.length,
14661
15824
  counts: eventCounts(completed),
14662
15825
  rows: [],
14663
15826
  replayed: true
@@ -14665,7 +15828,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14665
15828
  }
14666
15829
  let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
14667
15830
  if (!started) {
14668
- const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
15831
+ const preview = await previewNetworkPersonBulkImport(db, rows6, allowedChapterIds);
14669
15832
  if (preview.stateDigest !== input.expectedStateDigest) {
14670
15833
  throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
14671
15834
  }
@@ -14674,7 +15837,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14674
15837
  networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
14675
15838
  batchDigest,
14676
15839
  stateDigest: input.expectedStateDigest,
14677
- rowCount: rows5.length,
15840
+ rowCount: rows6.length,
14678
15841
  previewCounts: preview.counts
14679
15842
  })
14680
15843
  ], {
@@ -14690,7 +15853,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14690
15853
  if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
14691
15854
  throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
14692
15855
  }
14693
- const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
15856
+ const classified = await classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state);
14694
15857
  const results = [];
14695
15858
  const counts = networkPersonBulkEmptyCounts();
14696
15859
  for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
@@ -14729,10 +15892,10 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14729
15892
  }
14730
15893
  if (counts.failed === 0) {
14731
15894
  await db.transact([
14732
- networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
15895
+ networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows6.length + 1, {
14733
15896
  batchDigest,
14734
15897
  stateDigest: input.expectedStateDigest,
14735
- rowCount: rows5.length,
15898
+ rowCount: rows6.length,
14736
15899
  counts
14737
15900
  })
14738
15901
  ], { mutationId: completedMutationId });
@@ -14741,7 +15904,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14741
15904
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14742
15905
  batchDigest,
14743
15906
  stateDigest: input.expectedStateDigest,
14744
- rowCount: rows5.length,
15907
+ rowCount: rows6.length,
14745
15908
  counts,
14746
15909
  rows: results
14747
15910
  };
@@ -14862,7 +16025,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
14862
16025
  }
14863
16026
 
14864
16027
  // src/worker-routes-network-person-registry.ts
14865
- var PATH6 = "/api/admin/network-person-registry";
16028
+ var PATH7 = "/api/admin/network-person-registry";
14866
16029
  var MAX_BODY2 = 8 * 1024 * 1024;
14867
16030
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
14868
16031
  var positiveRevision = (value) => {
@@ -14887,7 +16050,7 @@ function registryError(error) {
14887
16050
  return null;
14888
16051
  }
14889
16052
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14890
- if (url.pathname !== PATH6) return null;
16053
+ if (url.pathname !== PATH7) return null;
14891
16054
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
14892
16055
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
14893
16056
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -14899,9 +16062,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14899
16062
  if (req.method === "POST") {
14900
16063
  let body;
14901
16064
  try {
14902
- const text10 = await req.text();
14903
- if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14904
- const parsed = JSON.parse(text10);
16065
+ const text11 = await req.text();
16066
+ if (new TextEncoder().encode(text11).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
16067
+ const parsed = JSON.parse(text11);
14905
16068
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14906
16069
  body = parsed;
14907
16070
  } catch {
@@ -15070,7 +16233,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
15070
16233
  // src/network-commercial-parity.ts
15071
16234
  var DIGEST = /^[0-9a-f]{64}$/;
15072
16235
  var MAX_RECORDS = 500;
15073
- var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
16236
+ var text9 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
15074
16237
  function parityMessage(partition, kind, value) {
15075
16238
  return [
15076
16239
  "chapter-commercial-parity",
@@ -15102,18 +16265,18 @@ async function digestWithKey(key3, partition, kind, value) {
15102
16265
  async function buildChapterCommercialParitySnapshot(input) {
15103
16266
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
15104
16267
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
15105
- const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
16268
+ const eligible = input.rows.filter((row2) => text9(row2.id) && (text9(row2.stripeCustomerId) || text9(row2.stripeSubscriptionId)));
15106
16269
  const selected2 = eligible.slice(0, maxRecords);
15107
16270
  const key3 = await parityKey(input.secret);
15108
16271
  const records = await Promise.all(selected2.map(async (row2) => {
15109
- const applicationId = text8(row2.id);
15110
- const customerId = text8(row2.stripeCustomerId);
15111
- const subscriptionId = text8(row2.stripeSubscriptionId);
16272
+ const applicationId = text9(row2.id);
16273
+ const customerId = text9(row2.stripeCustomerId);
16274
+ const subscriptionId = text9(row2.stripeSubscriptionId);
15112
16275
  return {
15113
16276
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
15114
16277
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
15115
16278
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
15116
- status: text8(row2.status, 64),
16279
+ status: text9(row2.status, 64),
15117
16280
  canceled: row2.canceled === true
15118
16281
  };
15119
16282
  }));
@@ -15139,7 +16302,7 @@ function validSnapshot(snapshot, source) {
15139
16302
  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;
15140
16303
  const seen = /* @__PURE__ */ new Set();
15141
16304
  for (const record4 of records) {
15142
- 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;
16305
+ 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;
15143
16306
  const key3 = recordKey(record4);
15144
16307
  if (seen.has(key3)) return false;
15145
16308
  seen.add(key3);
@@ -15150,16 +16313,16 @@ async function reconcileCommercialParitySnapshot(input) {
15150
16313
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
15151
16314
  const sourceKey = networkCommercialSourceKey(input.source);
15152
16315
  const key3 = await parityKey(input.secret);
15153
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
15154
- const customerId = text8(row2.customerId);
15155
- const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
16316
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text9(row2.applicationId) && text9(row2.objectId)).map(async (row2) => {
16317
+ const customerId = text9(row2.customerId);
16318
+ const subscriptionId = text9(row2.subscriptionId) ?? text9(row2.objectId);
15156
16319
  return {
15157
16320
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
15158
16321
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
15159
16322
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
15160
16323
  status: null,
15161
16324
  canceled: false,
15162
- stripeStatus: text8(row2.status, 64),
16325
+ stripeStatus: text9(row2.status, 64),
15163
16326
  stripeCanceled: row2.status === "canceled"
15164
16327
  };
15165
16328
  }));
@@ -15241,14 +16404,14 @@ var PAGE2 = 500;
15241
16404
  var MAX_SCAN = 5e3;
15242
16405
  var MAX_RECORDS2 = 500;
15243
16406
  async function localCommercialRows(db) {
15244
- const rows5 = [];
16407
+ const rows6 = [];
15245
16408
  for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
15246
16409
  const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
15247
16410
  const page = Array.isArray(result.applications) ? result.applications : [];
15248
- rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
15249
- if (page.length < PAGE2) return { rows: rows5, truncated: false };
16411
+ rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
16412
+ if (page.length < PAGE2) return { rows: rows6, truncated: false };
15250
16413
  }
15251
- return { rows: rows5, truncated: true };
16414
+ return { rows: rows6, truncated: true };
15252
16415
  }
15253
16416
  var handleNetworkCommercialParity = async (req, url, env, ctx) => {
15254
16417
  if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
@@ -15383,8 +16546,8 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15383
16546
  const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
15384
16547
  return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
15385
16548
  }
15386
- const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15387
- const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
16549
+ const rows6 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
16550
+ const member = rows6.find((row2) => disputedChargeCoversMembership(charge, row2));
15388
16551
  if (!member) return { disposition: "not_applicable" };
15389
16552
  const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
15390
16553
  return { disposition: "applied", membershipId: receipt.membershipId };
@@ -15628,15 +16791,15 @@ var handleFormation = async (req, url, env, ctx) => {
15628
16791
  if (submissionId && !SUBMISSION.test(submissionId)) {
15629
16792
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
15630
16793
  }
15631
- const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
15632
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
16794
+ const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
16795
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
15633
16796
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
15634
16797
  for (const field of formation.required) {
15635
16798
  const value = body[field];
15636
16799
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
15637
16800
  }
15638
16801
  const input = {};
15639
- for (const field of allowed2) {
16802
+ for (const field of allowed) {
15640
16803
  const value = body[field];
15641
16804
  if (value === void 0) continue;
15642
16805
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -15700,10 +16863,10 @@ var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
15700
16863
  var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
15701
16864
  var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
15702
16865
  var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
15703
- function unknownKeys3(value, allowed2, path, errors) {
15704
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
16866
+ function unknownKeys3(value, allowed, path, errors) {
16867
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
15705
16868
  }
15706
- function text9(value, path, errors, max = MAX_TEXT2, required = true) {
16869
+ function text10(value, path, errors, max = MAX_TEXT2, required = true) {
15707
16870
  if (value === void 0 && !required) return void 0;
15708
16871
  if (typeof value !== "string" || required && !value.trim()) {
15709
16872
  errors.push(`${path} must be a non-empty string`);
@@ -15729,7 +16892,7 @@ function scopeValue(value, path, errors) {
15729
16892
  return { kind: raw2.kind };
15730
16893
  }
15731
16894
  if (raw2.kind === "chapter") {
15732
- const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
16895
+ const chapterId = text10(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15733
16896
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15734
16897
  return { kind: "chapter", chapterId };
15735
16898
  }
@@ -15743,22 +16906,22 @@ function resourceValue(value, path, errors) {
15743
16906
  return null;
15744
16907
  }
15745
16908
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15746
- const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
16909
+ const id7 = text10(raw2.id, `${path}.id`, errors, 80) ?? "";
15747
16910
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15748
- const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
16911
+ const kind = text10(raw2.kind, `${path}.kind`, errors) ?? "";
15749
16912
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15750
- const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
15751
- const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
15752
- const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
16913
+ const title = text10(raw2.title, `${path}.title`, errors, 240) ?? "";
16914
+ const summary = text10(raw2.summary, `${path}.summary`, errors, 1e3, false);
16915
+ const status2 = text10(raw2.status, `${path}.status`, errors) ?? "";
15753
16916
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15754
16917
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15755
16918
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15756
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
16919
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text10(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15757
16920
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15758
16921
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15759
16922
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15760
- const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15761
- const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
16923
+ const bodyMarkdown = text10(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
16924
+ const url = text10(raw2.url, `${path}.url`, errors, 2048, false);
15762
16925
  if (url) {
15763
16926
  try {
15764
16927
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15770,9 +16933,9 @@ function resourceValue(value, path, errors) {
15770
16933
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15771
16934
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15772
16935
  const brandAsset = asset ? {
15773
- bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15774
- assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15775
- ...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
16936
+ bookId: text10(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
16937
+ assetId: text10(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
16938
+ ...asset.filename === void 0 ? {} : { filename: text10(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15776
16939
  } : void 0;
15777
16940
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15778
16941
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15787,17 +16950,17 @@ function resourceValue(value, path, errors) {
15787
16950
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15788
16951
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15789
16952
  const promotedFrom = promotedRaw ? {
15790
- chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15791
- resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
16953
+ chapterId: text10(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
16954
+ resourceId: text10(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15792
16955
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15793
16956
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15794
- promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
16957
+ promotedBy: text10(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15795
16958
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15796
16959
  } : void 0;
15797
16960
  const provenance2 = {
15798
- createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
16961
+ createdBy: text10(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15799
16962
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15800
- updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
16963
+ updatedBy: text10(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15801
16964
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15802
16965
  ...promotedFrom ? { promotedFrom } : {}
15803
16966
  };
@@ -15816,7 +16979,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15816
16979
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15817
16980
  const sourceType = sourceRaw?.type;
15818
16981
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15819
- const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
16982
+ const sourceAppId = text10(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15820
16983
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15821
16984
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15822
16985
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15824,8 +16987,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15824
16987
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15825
16988
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15826
16989
  const target = targetRaw ? {
15827
- appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15828
- environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
16990
+ appId: text10(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
16991
+ environment: text10(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15829
16992
  } : void 0;
15830
16993
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15831
16994
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15913,17 +17076,17 @@ async function builderLibraryDigest(document) {
15913
17076
  var control2 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
15914
17077
  var local3 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
15915
17078
  async function loadActiveBuilderLibraryControls(db, targetId, environment) {
15916
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
15917
- return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
17079
+ const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
17080
+ return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
15918
17081
  }
15919
17082
  async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
15920
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
15921
- return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
17083
+ const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
17084
+ return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
15922
17085
  }
15923
17086
  async function loadLocalBuilderLibrary(db, appId, environment) {
15924
17087
  const id7 = `builder-library-local:${environment}`;
15925
- const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
15926
- return local3(Array.isArray(rows5) ? rows5[0] : void 0);
17088
+ const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
17089
+ return local3(Array.isArray(rows6) ? rows6[0] : void 0);
15927
17090
  }
15928
17091
  async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
15929
17092
  const { document, digest } = envelope;
@@ -17566,8 +18729,8 @@ var readable = /* @__PURE__ */ new Set([
17566
18729
  "networkCommercialEvents",
17567
18730
  "networkCommercialExceptions"
17568
18731
  ]);
17569
- var check = (ns, allowed2) => {
17570
- if (!allowed2.has(ns)) throw new TypeError("namespace outside membership authority contract");
18732
+ var check = (ns, allowed) => {
18733
+ if (!allowed.has(ns)) throw new TypeError("namespace outside membership authority contract");
17571
18734
  return physical(ns);
17572
18735
  };
17573
18736
  function createMembershipAuthorityStorage(db, secretNames) {
@@ -17593,9 +18756,9 @@ function createMembershipAuthorityStorage(db, secretNames) {
17593
18756
  const guards = opts?.guards?.map((guard) => ({ ...guard, ns: check(guard.ns, readable) }));
17594
18757
  return db.transact(mapped, { ...opts, ...guards ? { guards } : {} });
17595
18758
  },
17596
- secrets: { get(name) {
17597
- if (!secrets.has(name)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
17598
- return db.secrets.get(name);
18759
+ secrets: { get(name2) {
18760
+ if (!secrets.has(name2)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
18761
+ return db.secrets.get(name2);
17599
18762
  } }
17600
18763
  };
17601
18764
  }
@@ -17765,7 +18928,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17765
18928
  const origin = new URL(options.origin), limit = options.limit ?? 100;
17766
18929
  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");
17767
18930
  const db = ctx.makeDb(env);
17768
- const rows5 = (await db.query({ applications: { $: {
18931
+ const rows6 = (await db.query({ applications: { $: {
17769
18932
  where: {
17770
18933
  groupId: ctx.chapter.id,
17771
18934
  stripeRuntime: env.ODLA_RUNTIME ?? "default",
@@ -17776,7 +18939,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17776
18939
  limit: limit + 1
17777
18940
  } } })).applications ?? [];
17778
18941
  let repaired = 0, pending = 0;
17779
- for (const app of rows5.slice(0, limit)) {
18942
+ for (const app of rows6.slice(0, limit)) {
17780
18943
  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");
17781
18944
  try {
17782
18945
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { approvalEffectsLastAttemptAt: Date.now() } }], {
@@ -17801,7 +18964,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17801
18964
  pending++;
17802
18965
  }
17803
18966
  }
17804
- return { repaired, pending, more: rows5.length > limit };
18967
+ return { repaired, pending, more: rows6.length > limit };
17805
18968
  }
17806
18969
 
17807
18970
  // src/membership-chapter-delivery.ts
@@ -17817,7 +18980,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17817
18980
  };
17818
18981
  signal.addEventListener("abort", cancel, { once: true });
17819
18982
  let size = 0;
17820
- let text10 = "";
18983
+ let text11 = "";
17821
18984
  const decoder = new TextDecoder();
17822
18985
  try {
17823
18986
  while (true) {
@@ -17827,10 +18990,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17827
18990
  if (done) break;
17828
18991
  size += value.byteLength;
17829
18992
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17830
- text10 += decoder.decode(value, { stream: true });
18993
+ text11 += decoder.decode(value, { stream: true });
17831
18994
  }
17832
18995
  try {
17833
- return JSON.parse(text10 + decoder.decode());
18996
+ return JSON.parse(text11 + decoder.decode());
17834
18997
  } catch {
17835
18998
  throw new TypeError(`invalid ${label}`);
17836
18999
  }
@@ -17849,8 +19012,8 @@ import { validateEmailCopy as validateEmailCopy5 } from "@odla-ai/crm";
17849
19012
  function invitationDelivery(env, recipientEmail) {
17850
19013
  const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
17851
19014
  if (env.ODLA_ENV !== "dev" || !env.SEND_INVITATION_TEST) return normal;
17852
- const allowed2 = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
17853
- if (!allowed2.includes(recipientEmail.trim().toLowerCase())) return normal;
19015
+ const allowed = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
19016
+ if (!allowed.includes(recipientEmail.trim().toLowerCase())) return normal;
17854
19017
  const to = env.ODLA_INVITATION_TEST_TO?.trim();
17855
19018
  const from = env.ODLA_INVITATION_TEST_FROM?.trim();
17856
19019
  const valid = (value) => value && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(value);
@@ -18370,27 +19533,27 @@ function validatedTokens(input) {
18370
19533
  }
18371
19534
  const tokens = {};
18372
19535
  let serializedLength = 0;
18373
- for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
18374
- if (!TOKEN_NAME.test(name)) {
18375
- throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
19536
+ for (const [name2, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
19537
+ if (!TOKEN_NAME.test(name2)) {
19538
+ throw new TypeError(`signup-control theme token "${name2}" must be an allowlisted --ui-* property`);
18376
19539
  }
18377
19540
  if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
18378
- throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
19541
+ throw new TypeError(`signup-control theme token "${name2}" must be a finite string or number`);
18379
19542
  }
18380
19543
  const value = String(raw2).trim();
18381
19544
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
18382
- throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
19545
+ throw new TypeError(`signup-control theme token "${name2}" has an unsafe or oversized value`);
18383
19546
  }
18384
- serializedLength += name.length + value.length + 4;
19547
+ serializedLength += name2.length + value.length + 4;
18385
19548
  if (serializedLength > MAX_SERIALIZED_LENGTH) {
18386
19549
  throw new TypeError("signup-control theme exceeds the serialized size bound");
18387
19550
  }
18388
- tokens[name] = value;
19551
+ tokens[name2] = value;
18389
19552
  }
18390
19553
  return tokens;
18391
19554
  }
18392
19555
  function cssFor(tokens) {
18393
- const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
19556
+ const declarations = Object.entries(tokens).map(([name2, value]) => ` ${name2}: ${value};`);
18394
19557
  return declarations.length ? `:root {
18395
19558
  ${declarations.join("\n")}
18396
19559
  }` : "";
@@ -18505,6 +19668,7 @@ var BUILTIN_ROUTES = [
18505
19668
  handleFormation,
18506
19669
  handleMember,
18507
19670
  handleMemberContent,
19671
+ handleMemberPortal,
18508
19672
  handleGifts,
18509
19673
  handleNamedSeat,
18510
19674
  handleNamedSeatRegift,