@odla-ai/chapter 0.61.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,9 +1648,9 @@ 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
 
@@ -1660,8 +1675,8 @@ function lineHtml(line) {
1660
1675
  }
1661
1676
  return out + escapeHtml(line.slice(last));
1662
1677
  }
1663
- function textToHtml(text10) {
1664
- const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1678
+ function textToHtml(text11) {
1679
+ const paragraphs = text11.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1665
1680
  const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
1666
1681
  return `<!doctype html>
1667
1682
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
@@ -1676,7 +1691,7 @@ function render(template, vars) {
1676
1691
  }
1677
1692
  var HTML_BODY_LIMIT = 2e5;
1678
1693
  function renderHtmlBody(template, vars) {
1679
- const escaped = Object.fromEntries(Object.entries(vars).map(([name, value]) => [name, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
1694
+ const escaped = Object.fromEntries(Object.entries(vars).map(([name2, value]) => [name2, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
1680
1695
  return renderTemplate(template, escaped);
1681
1696
  }
1682
1697
  function htmlBodyIssues(html, options) {
@@ -1686,7 +1701,7 @@ function htmlBodyIssues(html, options) {
1686
1701
  const unknown = new Set(templateVars(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
1687
1702
  for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
1688
1703
  if (options.values) {
1689
- for (const name of templateSectionNames(html)) if (typeof options.values[name] !== "string") issues.push({ code: "value-missing", variable: name, message: `Missing event value: ${name} (HTML body).` });
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).` });
1690
1705
  for (const variable of templateLiveVars(html, options.values)) {
1691
1706
  if (unknown.has(variable)) continue;
1692
1707
  const value = options.values[variable];
@@ -1766,12 +1781,12 @@ function planDelivery(input) {
1766
1781
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1767
1782
  if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1768
1783
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1769
- const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
1784
+ const text11 = redirect ? `(dev redirect; original recipient: ${input.to})
1770
1785
 
1771
1786
  ` + renderedText : renderedText;
1772
1787
  const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
1773
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;
1774
- return { deliver: true, transport, to, subject, text: text10, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
1789
+ return { deliver: true, transport, to, subject, text: text11, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
1775
1790
  }
1776
1791
 
1777
1792
  // src/email-deliverability.ts
@@ -1807,9 +1822,9 @@ function tierPayable(tier, group, hasSecretKey) {
1807
1822
  function byOrder(a, b) {
1808
1823
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1809
1824
  }
1810
- function resolveTiers(group, rows5 = []) {
1811
- const stored = rows5.filter((tier) => tier.active);
1812
- 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);
1813
1828
  return [{
1814
1829
  id: "standard",
1815
1830
  groupId: String(group.id ?? ""),
@@ -1865,16 +1880,16 @@ function rowToTier(row2, groupId) {
1865
1880
  }
1866
1881
  async function loadTiers(db, group) {
1867
1882
  const groupId = String(group.id ?? "");
1868
- let rows5;
1883
+ let rows6;
1869
1884
  try {
1870
- rows5 = (await db.query({
1885
+ rows6 = (await db.query({
1871
1886
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
1872
1887
  })).tiers;
1873
1888
  } catch {
1874
1889
  throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
1875
1890
  }
1876
- if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1877
- 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));
1878
1893
  return resolveTiers(group, list);
1879
1894
  }
1880
1895
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -1939,8 +1954,8 @@ function formatInstant(at2, timeZone) {
1939
1954
  var adminUrlFor = (origin) => `${origin}/admin/`;
1940
1955
  var membersUrlFor = (origin) => `${origin}/members/`;
1941
1956
  function applicantVariables(app) {
1942
- const text10 = (v) => typeof v === "string" ? v : "";
1943
- 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) };
1944
1959
  }
1945
1960
  function sampleVariables(origin, timeZone, now = Date.now()) {
1946
1961
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -1986,8 +2001,8 @@ async function emailTierVariables(db, input) {
1986
2001
  if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
1987
2002
  const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
1988
2003
  if (!tierId) return { tierName: "", tier: "" };
1989
- const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
1990
- 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() : "";
1991
2006
  return { tierName: tierName2, tier: tierName2, tierId };
1992
2007
  } catch {
1993
2008
  return { tierName: "", tier: "" };
@@ -1995,8 +2010,8 @@ async function emailTierVariables(db, input) {
1995
2010
  }
1996
2011
 
1997
2012
  // src/notify.ts
1998
- function fromAddress(configured, name) {
1999
- const display = name?.trim();
2013
+ function fromAddress(configured, name2) {
2014
+ const display = name2?.trim();
2000
2015
  if (!display) return configured;
2001
2016
  const angled = configured.match(/<([^>]+)>/);
2002
2017
  return { name: display, email: (angled ? angled[1] : configured).trim() };
@@ -2067,14 +2082,14 @@ async function sendTemplated(deps, input) {
2067
2082
  if (decision.transport === "log-only") return { sent: true };
2068
2083
  let messageId;
2069
2084
  try {
2070
- const text10 = String(row2.body);
2071
- const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text10);
2085
+ const text11 = String(row2.body);
2086
+ const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text11);
2072
2087
  const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
2073
2088
  const response = await deps.sender.send({
2074
2089
  from: fromAddress(deps.from, input.group.fromName),
2075
2090
  to: [decision.to],
2076
2091
  subject: String(row2.subject),
2077
- text: text10,
2092
+ text: text11,
2078
2093
  ...html ? { html } : {},
2079
2094
  ...headers ? { headers } : {},
2080
2095
  replyTo: input.group.replyTo
@@ -2354,13 +2369,13 @@ async function authoritativeState(db, input) {
2354
2369
  return state;
2355
2370
  }
2356
2371
  async function currentPolicy(db, input, restartAware, state) {
2357
- const rows5 = await db.query({
2372
+ const rows6 = await db.query({
2358
2373
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
2359
2374
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
2360
2375
  });
2361
- if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2362
- 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));
2363
- 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);
2364
2379
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
2365
2380
  const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
2366
2381
  const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
@@ -2478,13 +2493,13 @@ async function membershipRestartPayment(db, target, member, now) {
2478
2493
  // src/checkout-named-seat.ts
2479
2494
  function checkoutNamedSeatSelection(value, purchaserEmail) {
2480
2495
  const row2 = value;
2481
- 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, " ") : "";
2482
2497
  const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
2483
2498
  const purchaser = normalizeChapterEmail(purchaserEmail);
2484
- 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) {
2485
2500
  throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
2486
2501
  }
2487
- return { recipientName: name, recipientEmail: email };
2502
+ return { recipientName: name2, recipientEmail: email };
2488
2503
  }
2489
2504
  async function checkoutNamedSeatIdentity(target, email) {
2490
2505
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -2925,14 +2940,14 @@ async function readMembershipAccess(db, input, now) {
2925
2940
  const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
2926
2941
  requireMembershipScope(entitlement, input);
2927
2942
  const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
2928
- 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;
2929
2944
  return {
2930
2945
  membershipId: id7,
2931
- allowed: allowed2,
2946
+ allowed,
2932
2947
  status: String(member.status),
2933
2948
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
2934
2949
  revision: Number(member.revision),
2935
- restartEligible: !allowed2 && membershipCanRestart(member, now)
2950
+ restartEligible: !allowed && membershipCanRestart(member, now)
2936
2951
  };
2937
2952
  }
2938
2953
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -3077,11 +3092,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
3077
3092
  if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
3078
3093
  throw new Error("Stripe returned an invalid commercial list response");
3079
3094
  }
3080
- const rows5 = page.data.map(asRaw).filter((row2) => row2 !== null);
3081
- if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3082
- 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);
3083
3098
  if (!page.has_more) return { objects, complete: true };
3084
- const cursor = rows5.at(-1)?.id;
3099
+ const cursor = rows6.at(-1)?.id;
3085
3100
  if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
3086
3101
  startingAfter = cursor;
3087
3102
  }
@@ -3214,7 +3229,7 @@ async function approveApplication(input) {
3214
3229
  try {
3215
3230
  if (!duplicate) {
3216
3231
  const write = await db.transact(
3217
- [{ 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 } }],
3218
3233
  { mutationId: `approve:${id7}`, guards: [{
3219
3234
  ns: "applications",
3220
3235
  id: id7,
@@ -3497,8 +3512,8 @@ async function issueGiftClaim(db, intent, provider, now) {
3497
3512
  throw new Error("gift issuance conflict");
3498
3513
  }
3499
3514
  }
3500
- function buyerGiftClaims(rows5, now) {
3501
- return rows5.map((row2) => {
3515
+ function buyerGiftClaims(rows6, now) {
3516
+ return rows6.map((row2) => {
3502
3517
  const stored = String(row2.status ?? "pending");
3503
3518
  const status2 = stored === "pending" && Number(row2.codeExpiresAt) <= now ? "expired" : stored;
3504
3519
  return {
@@ -3644,8 +3659,8 @@ async function revokeGiftClaim(db, claim, reason, now) {
3644
3659
  // src/payments-route-support.ts
3645
3660
  async function findPaymentApplication(db, event2) {
3646
3661
  if ("applicationId" in event2 && event2.applicationId) {
3647
- const rows5 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3648
- return rows5.applications?.[0];
3662
+ const rows6 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3663
+ return rows6.applications?.[0];
3649
3664
  }
3650
3665
  if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
3651
3666
  const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
@@ -3836,6 +3851,306 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3836
3851
  return membershipFail("membership_dispute_retry_required");
3837
3852
  }
3838
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
+
3839
4154
  // src/member-system-contract.ts
3840
4155
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3841
4156
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3847,8 +4162,8 @@ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink)
3847
4162
 
3848
4163
  // src/stripe-webhook-partition.ts
3849
4164
  var firstRow = async (db, ns, q) => {
3850
- const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
3851
- 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;
3852
4167
  };
3853
4168
  var stripeEventIdentity = (event2) => ({
3854
4169
  ...event2.appId ? { appId: event2.appId } : {},
@@ -3955,8 +4270,8 @@ async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest,
3955
4270
  // src/stripe-event-reconcile.ts
3956
4271
  async function firstRow2(db, ns, q) {
3957
4272
  const res = await db.query({ [ns]: { $: q } });
3958
- const rows5 = res[ns];
3959
- return Array.isArray(rows5) ? rows5[0] : void 0;
4273
+ const rows6 = res[ns];
4274
+ return Array.isArray(rows6) ? rows6[0] : void 0;
3960
4275
  }
3961
4276
  async function reconcileChapterStripeEvent(input, env, ctx) {
3962
4277
  const db = ctx.makeDb(env);
@@ -4273,19 +4588,19 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
4273
4588
  const db = raw2, target = stripeEventTarget(env, ctx);
4274
4589
  const local4 = (row3) => row3.chapterId === target.chapterId && row3.appId === target.appId && row3.environment === target.environment && row3.runtime === target.runtime;
4275
4590
  if (req.method === "GET") {
4276
- const rows5 = (await db.query({ stripeEventReceipts: { $: {
4591
+ const rows6 = (await db.query({ stripeEventReceipts: { $: {
4277
4592
  where: { chapterId: target.chapterId, runtime: target.runtime },
4278
4593
  order: { receivedAt: "desc" },
4279
4594
  limit: 500
4280
4595
  } } })).stripeEventReceipts ?? [];
4281
- 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));
4282
4597
  return json({ rows: pending.map((row3) => ({
4283
4598
  eventId: row3.eventId,
4284
4599
  kind: row3.kind,
4285
4600
  disposition: row3.disposition,
4286
4601
  receivedAt: row3.receivedAt,
4287
4602
  retryReady: row3.retryReady === true
4288
- })), truncated: rows5.length >= 500 });
4603
+ })), truncated: rows6.length >= 500 });
4289
4604
  }
4290
4605
  let eventId;
4291
4606
  try {
@@ -4471,9 +4786,9 @@ function memberApplication(app, meeting, defaultTimezone) {
4471
4786
  var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
4472
4787
  async function memberSessionApplication(db, chapterId, runtime, identity2) {
4473
4788
  const lookup = async (where) => {
4474
- const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4475
- if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
4476
- 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));
4477
4792
  };
4478
4793
  let apps = await lookup({ clerkUserId: identity2.userId });
4479
4794
  let emailFallback = false;
@@ -4850,10 +5165,10 @@ async function namedSeatRecipientClaimId(input, email) {
4850
5165
  }
4851
5166
  async function reserveNamedMembershipSeat(db, input, now) {
4852
5167
  membershipClock(now);
4853
- 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, " ") : "";
4854
5169
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4855
5170
  const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4856
- 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");
4857
5172
  const state = await loadMembershipAuthorityState(db, input);
4858
5173
  const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4859
5174
  requireMembershipScope(member, input);
@@ -4870,7 +5185,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4870
5185
  id: id7,
4871
5186
  ...membershipScope(input),
4872
5187
  primaryMembershipId: memberId,
4873
- recipientName: name,
5188
+ recipientName: name2,
4874
5189
  recipientEmailNormalized: email,
4875
5190
  purchaserUserId: input.purchaserUserId,
4876
5191
  recipientClaimId: claimId,
@@ -4917,7 +5232,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4917
5232
  if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4918
5233
  }
4919
5234
  requireMembershipScope(seat, input);
4920
- 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");
4921
5236
  return seat;
4922
5237
  }
4923
5238
 
@@ -5108,9 +5423,9 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5108
5423
  membershipClock(now);
5109
5424
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
5110
5425
  const state = await loadMembershipAuthorityState(db, scope2);
5111
- 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 ?? [];
5112
5427
  let lifted = 0;
5113
- for (const member of rows5.slice(0, limit)) {
5428
+ for (const member of rows6.slice(0, limit)) {
5114
5429
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
5115
5430
  const offering = membershipOfferingVersion(member.offeringSnapshot);
5116
5431
  if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
@@ -5126,7 +5441,7 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5126
5441
  ] });
5127
5442
  lifted++;
5128
5443
  }
5129
- return { lifted, more: rows5.length > limit };
5444
+ return { lifted, more: rows6.length > limit };
5130
5445
  }
5131
5446
 
5132
5447
  // src/membership-seat-reprice.ts
@@ -5139,9 +5454,9 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
5139
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];
5140
5455
  if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
5141
5456
  const target = current.seatPriceCents;
5142
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5143
- const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
5144
- 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)) {
5145
5460
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
5146
5461
  const id7 = String(member.id), skip = (reason) => {
5147
5462
  receipt.skipped.push({ membershipId: id7, reason });
@@ -5240,8 +5555,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
5240
5555
  }
5241
5556
  }
5242
5557
  if (!one) {
5243
- const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5244
- 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 });
5245
5560
  }
5246
5561
  const seat = await membershipLoad(db, "networkSeat", one[1]);
5247
5562
  if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
@@ -5460,8 +5775,8 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5460
5775
  const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
5461
5776
  const found = await db.query({ networkMembership: page, networkSeat: page });
5462
5777
  const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
5463
- const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5464
- 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));
5465
5780
  return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
5466
5781
  }
5467
5782
  const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
@@ -5511,9 +5826,9 @@ function stateOf(item, all, now, window, rivals) {
5511
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";
5512
5827
  var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
5513
5828
  var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
5514
- function normalize(rows5, collection, parse, invalid2) {
5829
+ function normalize(rows6, collection, parse, invalid2) {
5515
5830
  const out = [];
5516
- for (const row2 of rows5) {
5831
+ for (const row2 of rows6) {
5517
5832
  try {
5518
5833
  out.push({ value: parse(row2.snapshot), row: row2 });
5519
5834
  } catch (error) {
@@ -5524,11 +5839,11 @@ function normalize(rows5, collection, parse, invalid2) {
5524
5839
  }
5525
5840
  async function listMembershipPolicyVersions(db, input) {
5526
5841
  const where = input.chapterId ? { chapterId: input.chapterId } : {};
5527
- const rows5 = await db.query({
5842
+ const rows6 = await db.query({
5528
5843
  membershipOfferingVersion: { $: { where, limit: LIMIT } },
5529
5844
  discountPolicyVersion: { $: { where, limit: LIMIT } }
5530
5845
  });
5531
- const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
5846
+ const offeringRows = rows6.membershipOfferingVersion ?? [], policyRows = rows6.discountPolicyVersion ?? [];
5532
5847
  const invalid2 = [];
5533
5848
  const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
5534
5849
  const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
@@ -5668,22 +5983,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
5668
5983
  const skip = (membershipId, reason) => {
5669
5984
  receipt.skipped.push({ membershipId, reason });
5670
5985
  };
5671
- let rows5;
5986
+ let rows6;
5672
5987
  if (input.membershipIds) {
5673
- rows5 = [];
5988
+ rows6 = [];
5674
5989
  for (const id7 of input.membershipIds) {
5675
5990
  const member = await membershipLoad(db, "networkMembership", id7);
5676
5991
  if (!member) skip(id7, "not_found");
5677
5992
  else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
5678
- else rows5.push(member);
5993
+ else rows6.push(member);
5679
5994
  }
5680
5995
  } else {
5681
5996
  const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5682
5997
  if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
5683
5998
  receipt.more = found.length > limit;
5684
- rows5 = found.slice(0, limit);
5999
+ rows6 = found.slice(0, limit);
5685
6000
  }
5686
- for (const member of rows5) {
6001
+ for (const member of rows6) {
5687
6002
  const id7 = String(member.id);
5688
6003
  if (member.discountPolicyVersionId === policy2.id) {
5689
6004
  receipt.unchanged.push(id7);
@@ -5979,13 +6294,13 @@ var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
5979
6294
  function resolveApplication(a) {
5980
6295
  const required = a?.required ?? DEFAULT_REQUIRED;
5981
6296
  const optional = a?.optional ?? DEFAULT_OPTIONAL;
5982
- for (const [name, arr] of [["required", required], ["optional", optional]]) {
6297
+ for (const [name2, arr] of [["required", required], ["optional", optional]]) {
5983
6298
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
5984
- 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`);
5985
6300
  }
5986
6301
  const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
5987
6302
  if (unsafe) {
5988
- 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}`);
5989
6304
  }
5990
6305
  }
5991
6306
  const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
@@ -6037,14 +6352,14 @@ function hasDisclaimerAck(fields) {
6037
6352
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
6038
6353
  function applicantProfile(chapter, fields) {
6039
6354
  const app = chapter.application;
6040
- const allowed2 = (f) => app.profileFields.includes(f);
6355
+ const allowed = (f) => app.profileFields.includes(f);
6041
6356
  const profile = {};
6042
6357
  for (const f of [...app.required, ...app.optional]) {
6043
- if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
6358
+ if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
6044
6359
  const v = fields[f];
6045
6360
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
6046
6361
  }
6047
- 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);
6048
6363
  return Object.keys(profile).length > 0 ? profile : void 0;
6049
6364
  }
6050
6365
  async function submitApplication(db, chapter, fields, opts) {
@@ -6163,10 +6478,10 @@ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
6163
6478
  applications: { $: { where: { emailNormalized: email }, limit: 10 } },
6164
6479
  $users: { $: { where: { email }, limit: 2 } }
6165
6480
  });
6166
- const rows5 = data.applications ?? [], others = rows5.filter((row2) => row2.id !== applicationId);
6481
+ const rows6 = data.applications ?? [], others = rows6.filter((row2) => row2.id !== applicationId);
6167
6482
  const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
6168
6483
  if (others.length !== closed.length) return conflict();
6169
- 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);
6170
6485
  const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
6171
6486
  if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
6172
6487
  const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
@@ -6290,15 +6605,15 @@ async function signupControlDigest(document) {
6290
6605
 
6291
6606
  // src/admission-store-read.ts
6292
6607
  async function readAdmissionRows(db, namespace, where, maximum) {
6293
- const rows5 = [];
6608
+ const rows6 = [];
6294
6609
  for (let offset = 0; offset <= maximum; offset += 500) {
6295
6610
  const result = await db.query({ [namespace]: { $: { where, order: { id: "asc" }, limit: 500, offset } } });
6296
6611
  const page = Array.isArray(result[namespace]) ? result[namespace] : [];
6297
- rows5.push(...page);
6298
- 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");
6299
6614
  if (page.length < 500) break;
6300
6615
  }
6301
- return rows5;
6616
+ return rows6;
6302
6617
  }
6303
6618
 
6304
6619
  // src/admission-grants.ts
@@ -6381,7 +6696,7 @@ async function createAdmissionGrantEnvelope(document) {
6381
6696
  if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
6382
6697
  return { document: checked.value, digest: await signupControlDigest(checked.value) };
6383
6698
  }
6384
- var rows2 = (value) => Array.isArray(value) ? value : [];
6699
+ var rows3 = (value) => Array.isArray(value) ? value : [];
6385
6700
  function asGrant(row2) {
6386
6701
  if (typeof row2.id !== "string" || typeof row2.emailNormalized !== "string") return null;
6387
6702
  if (row2.sourceScope !== "local" && row2.sourceScope !== "parent") return null;
@@ -6392,7 +6707,7 @@ async function findAdmissionGrant(db, targetId, email, now) {
6392
6707
  const emailNormalized = normalizeAdmissionEmail(email);
6393
6708
  if (!isAdmissionEmail(emailNormalized)) return null;
6394
6709
  const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
6395
- const active = rows2(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6710
+ const active = rows3(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6396
6711
  grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
6397
6712
  )).sort((a, b) => {
6398
6713
  const scope2 = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
@@ -6406,7 +6721,7 @@ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
6406
6721
  return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
6407
6722
  }
6408
6723
  const found = await db.query({ admissionGrantRevisions: {} });
6409
- 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);
6410
6725
  }
6411
6726
  async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
6412
6727
  if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
@@ -6415,7 +6730,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
6415
6730
  order: { revision: "desc" },
6416
6731
  limit: 1
6417
6732
  } } });
6418
- 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];
6419
6734
  return row2 ? row2 : null;
6420
6735
  }
6421
6736
  async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
@@ -6494,8 +6809,8 @@ var APPLICATION_LABELS = {
6494
6809
  state: "State"
6495
6810
  };
6496
6811
  function humanize(id7) {
6497
- const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6498
- 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;
6499
6814
  }
6500
6815
  function applicationDefs(chapter) {
6501
6816
  try {
@@ -6588,8 +6903,8 @@ function validateGiftOfferCatalog(input, tierIds) {
6588
6903
  errors.push(`${path} must be an object`);
6589
6904
  continue;
6590
6905
  }
6591
- const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6592
- 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`);
6593
6908
  const id7 = text5(item.id, `${path}.id`, errors, 80);
6594
6909
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6595
6910
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
@@ -6653,27 +6968,27 @@ async function hasSignupControl(db, targetId, environment, active) {
6653
6968
  if (db.aggregate) {
6654
6969
  return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
6655
6970
  }
6656
- const rows5 = (await db.query({ signupControls: {} })).signupControls;
6657
- 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));
6658
6973
  }
6659
6974
  async function loadLatestSignupControl(db, targetId, environment) {
6660
6975
  if (!await hasSignupControl(db, targetId, environment)) return null;
6661
- const rows5 = (await db.query({ signupControls: { $: {
6976
+ const rows6 = (await db.query({ signupControls: { $: {
6662
6977
  where: { targetId, environment },
6663
6978
  order: { revision: "desc" },
6664
6979
  limit: 1
6665
6980
  } } })).signupControls;
6666
- 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;
6667
6982
  return asStored(latest);
6668
6983
  }
6669
6984
  async function loadActiveSignupControl(db, targetId, environment) {
6670
6985
  if (!await hasSignupControl(db, targetId, environment, true)) return null;
6671
- const rows5 = (await db.query({ signupControls: { $: {
6986
+ const rows6 = (await db.query({ signupControls: { $: {
6672
6987
  where: { targetId, environment, active: true },
6673
6988
  order: { revision: "desc" },
6674
6989
  limit: 1
6675
6990
  } } })).signupControls;
6676
- 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;
6677
6992
  return asStored(active);
6678
6993
  }
6679
6994
  function controlledApplication(base, document) {
@@ -7046,17 +7361,17 @@ async function readBoundedRequestText(req, maxBytes) {
7046
7361
  if (!reader) return "";
7047
7362
  const decoder = new TextDecoder();
7048
7363
  let bytes = 0;
7049
- let text10 = "";
7364
+ let text11 = "";
7050
7365
  for (; ; ) {
7051
7366
  const { done, value } = await reader.read();
7052
7367
  if (req.signal.aborted) return invalid2();
7053
7368
  if (done) {
7054
7369
  complete = true;
7055
- return text10 + decoder.decode();
7370
+ return text11 + decoder.decode();
7056
7371
  }
7057
7372
  bytes += value.byteLength;
7058
7373
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
7059
- text10 += decoder.decode(value, { stream: true });
7374
+ text11 += decoder.decode(value, { stream: true });
7060
7375
  }
7061
7376
  } catch {
7062
7377
  return invalid2();
@@ -7160,9 +7475,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
7160
7475
  let selection;
7161
7476
  if (request?.body) {
7162
7477
  try {
7163
- const text10 = await readBoundedRequestText(request, 2048);
7164
- if (text10 instanceof Response) return text10;
7165
- 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);
7166
7481
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
7167
7482
  selection = body.namedSeat;
7168
7483
  } catch {
@@ -7315,9 +7630,9 @@ async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
7315
7630
  if (!app || app.groupId !== chapterId || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(runtime)) {
7316
7631
  throw new Error("invitation purchaser unavailable");
7317
7632
  }
7318
- const name = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7319
- if (!name) throw new Error("invitation purchaser name unavailable");
7320
- 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);
7321
7636
  }
7322
7637
  async function namedSeatApplicationContext(req, seatId, env, ctx) {
7323
7638
  const seats = ctx.membershipAuthority?.namedSeats;
@@ -7346,10 +7661,10 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
7346
7661
  }
7347
7662
  async function existingNamedSeatApplication(context, env, ctx) {
7348
7663
  const { db, userId, email, invitation } = context;
7349
- const rows5 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7350
- 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));
7351
7666
  if (!apps.length) {
7352
- 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];
7353
7668
  return { predecessor: closed ? { id: String(closed.id) } : null };
7354
7669
  }
7355
7670
  const app = apps[0];
@@ -7714,6 +8029,111 @@ async function notifySubmitConfirmed(db, env, notice) {
7714
8029
  }
7715
8030
  }
7716
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
+
7717
8137
  // src/worker-routes-health.ts
7718
8138
  var handleConfig = async (_req, url, env, ctx) => {
7719
8139
  if (url.pathname !== "/api/config") return null;
@@ -7728,13 +8148,13 @@ var handleHealth = async (_req, url, env, ctx) => {
7728
8148
  if (url.pathname !== "/api/health") return null;
7729
8149
  const response = json({ ok: true });
7730
8150
  response.headers.set("cache-control", "no-store");
7731
- for (const [name, value] of Object.entries({
8151
+ for (const [name2, value] of Object.entries({
7732
8152
  "x-odla-app-id": env.ODLA_APP_ID,
7733
8153
  "x-odla-chapter-id": ctx.chapter.id,
7734
8154
  "x-odla-data-environment": env.ODLA_ENV,
7735
8155
  "x-odla-runtime": env.ODLA_RUNTIME || "default"
7736
8156
  })) {
7737
- 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);
7738
8158
  }
7739
8159
  return response;
7740
8160
  };
@@ -7748,8 +8168,10 @@ var handleMe = async (req, url, env, ctx) => {
7748
8168
  const db = ctx.makeDb(env);
7749
8169
  const role = await ctx.roleFor(db, u);
7750
8170
  const superAdmin = await ctx.isSuperAdminEmail(db, u.email, u.userId);
7751
- const base = { authorized: superAdmin || isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
7752
- 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));
7753
8175
  let member;
7754
8176
  try {
7755
8177
  member = await memberSessionApplication(
@@ -7785,7 +8207,8 @@ var handleMe = async (req, url, env, ctx) => {
7785
8207
  } catch {
7786
8208
  }
7787
8209
  }
7788
- 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") });
7789
8212
  };
7790
8213
  var handleCrm = async (req, url, env, ctx) => {
7791
8214
  const crmBase = ctx.crmBase;
@@ -8030,9 +8453,9 @@ var MAX_BENEFITS = 24;
8030
8453
  var MAX_MARKDOWN = 65536;
8031
8454
  var MAX_LABEL = 160;
8032
8455
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8033
- function rejectUnknownKeys(value, allowed2, path, errors) {
8456
+ function rejectUnknownKeys(value, allowed, path, errors) {
8034
8457
  for (const key3 of Object.keys(value)) {
8035
- if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8458
+ if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8036
8459
  }
8037
8460
  }
8038
8461
  function boundedString(value, path, errors, max, required = false) {
@@ -8044,10 +8467,10 @@ function boundedString(value, path, errors, max, required = false) {
8044
8467
  errors.push(`${path} must be a string`);
8045
8468
  return void 0;
8046
8469
  }
8047
- const text10 = value.trim();
8048
- if (required && !text10) errors.push(`${path} is required`);
8049
- if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8050
- 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;
8051
8474
  }
8052
8475
  function validateMembershipPageDocument(input, options = {}) {
8053
8476
  const errors = [];
@@ -8096,8 +8519,8 @@ function validateMembershipPageDocument(input, options = {}) {
8096
8519
  else {
8097
8520
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
8098
8521
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
8099
- const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8100
- return text10 ? [text10] : [];
8522
+ const text11 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8523
+ return text11 ? [text11] : [];
8101
8524
  });
8102
8525
  }
8103
8526
  }
@@ -8188,10 +8611,10 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
8188
8611
  ]);
8189
8612
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
8190
8613
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8191
- function unknownKeys(value, allowed2, path, errors) {
8192
- 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`);
8193
8616
  }
8194
- function text6(value, path, errors, max = 65536) {
8617
+ function text7(value, path, errors, max = 65536) {
8195
8618
  if (typeof value !== "string" || !value.trim()) {
8196
8619
  errors.push(`${path} is required`);
8197
8620
  return "";
@@ -8214,11 +8637,11 @@ function tiersOf(value, errors) {
8214
8637
  return [];
8215
8638
  }
8216
8639
  unknownKeys(item, TIER_KEYS2, path, errors);
8217
- const id7 = text6(item.id, `${path}.id`, errors, 80);
8640
+ const id7 = text7(item.id, `${path}.id`, errors, 80);
8218
8641
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8219
8642
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
8220
8643
  seen.add(id7);
8221
- const name = text6(item.name, `${path}.name`, errors, 160);
8644
+ const name2 = text7(item.name, `${path}.name`, errors, 160);
8222
8645
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
8223
8646
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
8224
8647
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -8236,10 +8659,10 @@ function tiersOf(value, errors) {
8236
8659
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
8237
8660
  errors.push(`${path}.interval must be year or month`);
8238
8661
  }
8239
- 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);
8240
8663
  return [{
8241
8664
  id: id7,
8242
- name,
8665
+ name: name2,
8243
8666
  available: item.available === true,
8244
8667
  sortOrder: Number(item.sortOrder) || 0,
8245
8668
  priceCents: Number(item.priceCents) || 0,
@@ -8282,7 +8705,7 @@ function policyOf(value, errors, tiers) {
8282
8705
  errors.push("document.policy must be an object");
8283
8706
  return void 0;
8284
8707
  }
8285
- const allowed2 = /* @__PURE__ */ new Set([
8708
+ const allowed = /* @__PURE__ */ new Set([
8286
8709
  "disclaimerText",
8287
8710
  "refundPolicyText",
8288
8711
  "merchantDisclosureText",
@@ -8290,11 +8713,11 @@ function policyOf(value, errors, tiers) {
8290
8713
  "commitmentText",
8291
8714
  "normsText"
8292
8715
  ]);
8293
- unknownKeys(input, /* @__PURE__ */ new Set([...allowed2, "tierPolicies"]), "document.policy", errors);
8716
+ unknownKeys(input, /* @__PURE__ */ new Set([...allowed, "tierPolicies"]), "document.policy", errors);
8294
8717
  const result = {};
8295
- for (const key3 of allowed2) {
8718
+ for (const key3 of allowed) {
8296
8719
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
8297
- result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
8720
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
8298
8721
  }
8299
8722
  const tierPolicies = {};
8300
8723
  if (input.tierPolicies !== void 0) {
@@ -8309,8 +8732,8 @@ function policyOf(value, errors, tiers) {
8309
8732
  }
8310
8733
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
8311
8734
  tierPolicies[id7] = {
8312
- refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8313
- ...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) } : {}
8314
8737
  };
8315
8738
  }
8316
8739
  }
@@ -8350,9 +8773,9 @@ function validateSignupControlDocument(input) {
8350
8773
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
8351
8774
  const source = recordOf(root.source);
8352
8775
  const target = recordOf(root.target);
8353
- const appId = text6(source?.appId, "document.source.appId", errors, 63);
8354
- const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
8355
- 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);
8356
8779
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
8357
8780
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
8358
8781
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -8493,14 +8916,45 @@ async function memberContentDigest(document) {
8493
8916
  return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
8494
8917
  }
8495
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
+
8496
8950
  // src/member-content-validation-helpers.ts
8497
8951
  var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
8498
8952
  var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8499
- 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"]);
8500
8954
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
8501
8955
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
8502
- var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
8503
- 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"]);
8504
8958
  var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
8505
8959
  var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
8506
8960
  var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
@@ -8509,8 +8963,8 @@ var MAX_PAGES = 100;
8509
8963
  var MAX_MARKDOWN2 = 65536;
8510
8964
  var MAX_TEXT = 240;
8511
8965
  var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8512
- function unknownKeys2(value, allowed2, path, errors) {
8513
- 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`);
8514
8968
  }
8515
8969
  function textValue(value, path, errors, max = MAX_TEXT, required = false) {
8516
8970
  if (value === void 0) {
@@ -8550,13 +9004,140 @@ function offerValue(value, path, errors, tiers, audience) {
8550
9004
  };
8551
9005
  }
8552
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
+
8553
9131
  // src/member-content-validation.ts
8554
9132
  function validateMemberContentDocument(input, options = {}) {
8555
9133
  const errors = [];
8556
9134
  const root = record2(input);
8557
9135
  if (!root) return { ok: false, errors: ["document must be an object"] };
8558
9136
  unknownKeys2(root, ROOT_KEYS3, "document", errors);
8559
- 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;
8560
9141
  if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
8561
9142
  if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
8562
9143
  const sourceRaw = record2(root.source);
@@ -8601,6 +9182,8 @@ function validateMemberContentDocument(input, options = {}) {
8601
9182
  continue;
8602
9183
  }
8603
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) : {};
8604
9187
  const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
8605
9188
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
8606
9189
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
@@ -8615,13 +9198,19 @@ function validateMemberContentDocument(input, options = {}) {
8615
9198
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
8616
9199
  else {
8617
9200
  unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
9201
+ if (!v2) rejectV2Fields(navRaw, V2_NAV_KEYS, `${path}.navigation`, errors);
8618
9202
  const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
8619
9203
  const order2 = navRaw.order;
8620
9204
  if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
8621
9205
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
8622
9206
  }
8623
9207
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
8624
- 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
+ };
8625
9214
  }
8626
9215
  const audienceRaw = record2(raw2.audience);
8627
9216
  let audience = { kind: "all-members" };
@@ -8647,7 +9236,7 @@ function validateMemberContentDocument(input, options = {}) {
8647
9236
  }
8648
9237
  const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
8649
9238
  const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
8650
- const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
9239
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, !v2);
8651
9240
  let upsell;
8652
9241
  if (raw2.upsell !== void 0) {
8653
9242
  const upsellRaw = record2(raw2.upsell);
@@ -8664,7 +9253,22 @@ function validateMemberContentDocument(input, options = {}) {
8664
9253
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
8665
9254
  }
8666
9255
  }
8667
- 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);
8668
9272
  }
8669
9273
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
8670
9274
  for (const [index2, page] of pages.entries()) {
@@ -8711,21 +9315,21 @@ function validateMemberContentDocument(input, options = {}) {
8711
9315
  }
8712
9316
  if (errors.length) return { ok: false, errors };
8713
9317
  return { ok: true, value: {
8714
- schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
9318
+ schemaVersion: v2 ? 2 : 1,
8715
9319
  revision: Number(root.revision),
8716
9320
  generatedAt: Number(root.generatedAt),
8717
9321
  source,
8718
9322
  ...target ? { target } : {},
8719
9323
  pages,
8720
- ...upsellOverrides ? { upsellOverrides } : {}
9324
+ ...upsellOverrides ? { upsellOverrides } : {},
9325
+ ...sections ? { sections } : {}
8721
9326
  } };
8722
9327
  }
8723
9328
 
8724
9329
  // src/member-content.ts
8725
- var MEMBER_CONTENT_SCHEMA_VERSION = 1;
8726
9330
  var MEMBER_CONTENT_PATH = "/api/member-content";
8727
9331
  var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
8728
- 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);
8729
9333
  function composeMemberContent(parent, chapter) {
8730
9334
  const pages = [];
8731
9335
  const parentPages = /* @__PURE__ */ new Map();
@@ -8744,20 +9348,24 @@ function composeMemberContent(parent, chapter) {
8744
9348
  }
8745
9349
  pages.push({ ...page, source: chapter.source });
8746
9350
  }
9351
+ const sections = composeMemberSections(parent, chapter, pages);
9352
+ const rank = new Map(sections.map((section, index2) => [section.id, index2]));
8747
9353
  return {
8748
- pages: pages.sort(pageOrder),
9354
+ pages: pages.sort(pageOrder(rank)),
9355
+ sections,
8749
9356
  parentRevision: parent?.revision ?? null,
8750
9357
  chapterRevision: chapter?.revision ?? null
8751
9358
  };
8752
9359
  }
8753
- 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);
8754
9361
  function projectMemberContent(content, viewer, tiers) {
8755
9362
  const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
8756
9363
  return {
8757
9364
  viewer: { ...viewer },
8758
9365
  revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
9366
+ sections: content.sections ?? composeMemberSections(null, null, content.pages),
8759
9367
  pages: content.pages.map((page) => {
8760
- const canRead = allowed(page, viewer);
9368
+ const canRead = memberPageAllowed(page, viewer);
8761
9369
  const offer = !canRead ? page.upsell?.offer : void 0;
8762
9370
  const tier = offer ? authority.get(offer.tierId) : void 0;
8763
9371
  const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
@@ -8770,8 +9378,11 @@ function projectMemberContent(content, viewer, tiers) {
8770
9378
  audience: page.audience,
8771
9379
  source: page.source,
8772
9380
  access: canRead ? "allowed" : "locked",
9381
+ kind: page.kind ?? "markdown",
9382
+ ...page.placeholder ? { placeholder: page.placeholder } : {},
9383
+ ...page.feature ? { feature: page.feature } : {},
8773
9384
  ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
8774
- ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
9385
+ ...canRead && page.bodyMarkdown !== void 0 ? { bodyMarkdown: page.bodyMarkdown } : {},
8775
9386
  ...upsell ? { upsell } : {}
8776
9387
  };
8777
9388
  })
@@ -8783,33 +9394,33 @@ var control = (row2) => row2 && row2.document && typeof row2.document === "objec
8783
9394
  var local2 = (row2) => row2 && row2.document && typeof row2.document === "object" ? row2 : null;
8784
9395
  async function exists(db, ns, where) {
8785
9396
  if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
8786
- const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
8787
- 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));
8788
9399
  }
8789
9400
  async function loadActiveMemberContentControl(db, targetId, environment) {
8790
9401
  const where = { targetId, environment, active: true };
8791
9402
  if (!await exists(db, "memberContentControls", where)) return null;
8792
- const rows5 = (await db.query({ memberContentControls: { $: {
9403
+ const rows6 = (await db.query({ memberContentControls: { $: {
8793
9404
  where,
8794
9405
  order: { revision: "desc" },
8795
9406
  limit: 1
8796
9407
  } } })).memberContentControls;
8797
- 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);
8798
9409
  }
8799
9410
  async function loadLatestMemberContentControl(db, targetId, environment) {
8800
9411
  const where = { targetId, environment };
8801
9412
  if (!await exists(db, "memberContentControls", where)) return null;
8802
- const rows5 = (await db.query({ memberContentControls: { $: {
9413
+ const rows6 = (await db.query({ memberContentControls: { $: {
8803
9414
  where,
8804
9415
  order: { revision: "desc" },
8805
9416
  limit: 1
8806
9417
  } } })).memberContentControls;
8807
- 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);
8808
9419
  }
8809
9420
  async function loadLocalMemberContent(db, chapterId, environment) {
8810
9421
  const id7 = `member-content-local:${environment}`;
8811
- const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
8812
- 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);
8813
9424
  }
8814
9425
  async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
8815
9426
  const { document, digest } = envelope;
@@ -8856,7 +9467,8 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
8856
9467
  var ADMIN_PATH = "/api/admin/member-content";
8857
9468
  var BODY_CAP = 1e6;
8858
9469
  var reject4 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
8859
- async function groupAndTiers(db, chapterId) {
9470
+ async function groupAndTiers(db, chapterId, mode = "chapter") {
9471
+ if (mode === "hub") return { group: null, tiers: [] };
8860
9472
  const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
8861
9473
  const group = Array.isArray(groups) ? groups[0] : void 0;
8862
9474
  if (!group) return { group: null, tiers: [] };
@@ -8893,7 +9505,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
8893
9505
  if (document.source.appId !== settings.sourceId) return reject4("wrong_source", 403);
8894
9506
  if (document.target?.chapterId !== ctx.chapter.id) return reject4("wrong_target", 409);
8895
9507
  if (document.target.environment !== env.ODLA_ENV) return reject4("wrong_environment", 409);
8896
- const { tiers } = await groupAndTiers(db, ctx.chapter.id);
9508
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode);
8897
9509
  const checked = validateMemberContentDocument(document, {
8898
9510
  sourceType: "parent",
8899
9511
  sourceAppId: settings.sourceId,
@@ -8947,17 +9559,13 @@ var handleMemberContent = async (req, url, env, ctx) => {
8947
9559
  const user = await ctx.verifyUser(req, env);
8948
9560
  if (!user) return json({ error: "unauthorized" }, 401);
8949
9561
  const db = ctx.makeDb(env);
8950
- let tierId;
8951
- try {
8952
- const member = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, { userId: user.userId, email: user.email });
8953
- if (!member || !await chapterMembershipAccess(db, env, ctx, member.source)) return json({ error: "member access required" }, 403);
8954
- tierId = typeof member.source.tierId === "string" ? member.source.tierId : null;
8955
- } catch {
8956
- return json({ error: "membership access unavailable" }, 503);
8957
- }
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;
8958
9566
  const [{ parent, chapter }, { group, tiers }] = await Promise.all([
8959
9567
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
8960
- groupAndTiers(db, ctx.chapter.id)
9568
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
8961
9569
  ]);
8962
9570
  const effective = composeMemberContent(parent?.document, chapter?.document);
8963
9571
  let offerable = tiers;
@@ -8976,7 +9584,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
8976
9584
  if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
8977
9585
  const [{ parent, chapter }, { tiers }] = await Promise.all([
8978
9586
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
8979
- groupAndTiers(db, ctx.chapter.id)
9587
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
8980
9588
  ]);
8981
9589
  if (req.method === "GET") {
8982
9590
  const effective = composeMemberContent(parent?.document, chapter?.document);
@@ -8984,7 +9592,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
8984
9592
  parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
8985
9593
  chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
8986
9594
  effective,
8987
- 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 })),
8988
9596
  localAuthoring: ctx.chapter.memberContent.localAuthoring
8989
9597
  });
8990
9598
  }
@@ -9011,6 +9619,8 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9011
9619
  parentPages: parent?.document.pages ?? []
9012
9620
  });
9013
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);
9014
9624
  try {
9015
9625
  const saved = await saveLocalMemberContent(
9016
9626
  db,
@@ -9032,13 +9642,521 @@ function withLocalAuthority(value, revision, generatedAt, appId) {
9032
9642
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
9033
9643
  return {
9034
9644
  ...value,
9035
- schemaVersion: 1,
9645
+ // The author chooses the contract version; anything else defaults to 1.
9646
+ schemaVersion: value.schemaVersion === 2 ? 2 : 1,
9036
9647
  revision,
9037
9648
  generatedAt,
9038
9649
  source: { type: "chapter", appId }
9039
9650
  };
9040
9651
  }
9041
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
+
9042
10160
  // src/worker-routes-admission-grants.ts
9043
10161
  import { verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
9044
10162
  var reject5 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
@@ -9419,9 +10537,9 @@ function integer3(value, fallback, min, max) {
9419
10537
  const parsed = Number(value);
9420
10538
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
9421
10539
  }
9422
- function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
10540
+ function projectRecord(record4, allowed, editable, stagesEnabled, crm) {
9423
10541
  const fields = Object.fromEntries(
9424
- 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]]] : [])
9425
10543
  );
9426
10544
  const def = crm.type(record4.type);
9427
10545
  const editFields = editable.map((id7) => {
@@ -9531,7 +10649,7 @@ async function deliverAdmissionGrants(options) {
9531
10649
  }
9532
10650
 
9533
10651
  // src/worker-routes-network-admission.ts
9534
- var PATH5 = "/api/admin/network/admissions";
10652
+ var PATH6 = "/api/admin/network/admissions";
9535
10653
  var RETRY_PATH = "/api/admin/network/admissions/retry";
9536
10654
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
9537
10655
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -9611,11 +10729,11 @@ async function parseBody(req) {
9611
10729
  }
9612
10730
  }
9613
10731
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
9614
- if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
10732
+ if (![PATH6, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
9615
10733
  const auth = await gate2(req, env, ctx);
9616
10734
  if ("response" in auth) return auth.response;
9617
10735
  const db = auth.db;
9618
- if (url.pathname === PATH5 && req.method === "GET") {
10736
+ if (url.pathname === PATH6 && req.method === "GET") {
9619
10737
  const targetId2 = url.searchParams.get("targetId") ?? "";
9620
10738
  const target2 = targetFor(ctx, targetId2);
9621
10739
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -9763,8 +10881,8 @@ function makeCalendar(env) {
9763
10881
  }
9764
10882
  async function firstRow3(db, ns, q) {
9765
10883
  const res = await db.query({ [ns]: { $: q } });
9766
- const rows5 = res[ns];
9767
- return Array.isArray(rows5) ? rows5[0] : void 0;
10884
+ const rows6 = res[ns];
10885
+ return Array.isArray(rows6) ? rows6[0] : void 0;
9768
10886
  }
9769
10887
  async function computeSlots(cal, cfg) {
9770
10888
  const { from, to } = slotWindow(Date.now(), cfg.windowDays);
@@ -10623,18 +11741,18 @@ async function startSubscription(req, env, ctx) {
10623
11741
 
10624
11742
  // src/worker-payment-receipt.ts
10625
11743
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10626
- 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]);
10627
11745
  async function paymentReceiptRoute(req, url, env, ctx) {
10628
11746
  const id7 = url.searchParams.get("application");
10629
11747
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
10630
11748
  if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
10631
11749
  const db = ctx.makeDb(env);
10632
- const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
10633
- 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];
10634
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);
10635
11753
  const receipt = intent.paymentReceipt;
10636
11754
  if (!receipt) return json2({ pending: true }, 202);
10637
- const text10 = paymentReceiptText(receipt, ctx.chapter.name);
11755
+ const text11 = paymentReceiptText(receipt, ctx.chapter.name);
10638
11756
  if (req.method === "POST") {
10639
11757
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10640
11758
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10652,7 +11770,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10652
11770
  template: "paymentReceipt",
10653
11771
  to: String(intent.purchaserEmailNormalized),
10654
11772
  applicationId: id7,
10655
- vars: { chapterName: ctx.chapter.name, receiptText: text10 },
11773
+ vars: { chapterName: ctx.chapter.name, receiptText: text11 },
10656
11774
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10657
11775
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10658
11776
  });
@@ -10662,7 +11780,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10662
11780
  const nonce = crypto.randomUUID();
10663
11781
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10664
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>
10665
- <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>
10666
11784
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10667
11785
  headers: {
10668
11786
  "content-type": "text/html; charset=utf-8",
@@ -10695,7 +11813,7 @@ var handlePayments = async (req, url, env, ctx) => {
10695
11813
 
10696
11814
  // src/worker-routes-gifts.ts
10697
11815
  var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
10698
- var rows3 = (value) => Array.isArray(value) ? value : [];
11816
+ var rows4 = (value) => Array.isArray(value) ? value : [];
10699
11817
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
10700
11818
  var userFor = async (req, env, ctx) => {
10701
11819
  const user = await ctx.verifyUser(req, env);
@@ -10733,7 +11851,7 @@ var handleGifts = async (req, url, env, ctx) => {
10733
11851
  const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
10734
11852
  return json({
10735
11853
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
10736
- 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()),
10737
11855
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
10738
11856
  merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
10739
11857
  });
@@ -10756,7 +11874,7 @@ var handleGifts = async (req, url, env, ctx) => {
10756
11874
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
10757
11875
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
10758
11876
  const emailNormalized = normalizeChapterEmail(user.email);
10759
- 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);
10760
11878
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
10761
11879
  try {
10762
11880
  return json(await startGiftIntent(db, {
@@ -10886,9 +12004,9 @@ var receiptFor2 = (seat, released) => ({
10886
12004
  });
10887
12005
  async function regiftNamedSeat(db, input, now) {
10888
12006
  membershipClock(now);
10889
- 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, " ") : "";
10890
12008
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
10891
- 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");
10892
12010
  const state = await loadMembershipAuthorityState(db, input);
10893
12011
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
10894
12012
  requireMembershipScope(seat, input);
@@ -10897,7 +12015,7 @@ async function regiftNamedSeat(db, input, now) {
10897
12015
  if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
10898
12016
  if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
10899
12017
  const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
10900
- 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") {
10901
12019
  return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
10902
12020
  }
10903
12021
  if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
@@ -10935,7 +12053,7 @@ async function regiftNamedSeat(db, input, now) {
10935
12053
  });
10936
12054
  const ops = [
10937
12055
  membershipUpdate("networkSeat", String(seat.id), {
10938
- recipientName: name,
12056
+ recipientName: name2,
10939
12057
  recipientEmailNormalized: email,
10940
12058
  recipientClaimId: claimId,
10941
12059
  status: "pending_acceptance",
@@ -11132,16 +12250,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
11132
12250
  };
11133
12251
 
11134
12252
  // src/named-seat-accepted-notice.ts
11135
- var text7 = (value) => typeof value === "string" ? value : "";
12253
+ var text8 = (value) => typeof value === "string" ? value : "";
11136
12254
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
11137
12255
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
11138
- 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)) {
11139
12257
  return { sent: false, reason: "purchaser-missing" };
11140
12258
  }
11141
12259
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
11142
12260
  if (!group) return { sent: false, reason: "group-missing" };
11143
12261
  const config = emailGroupFrom(group);
11144
- 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();
11145
12263
  return sendTemplated({
11146
12264
  db,
11147
12265
  envName: env.ODLA_ENV,
@@ -11153,12 +12271,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
11153
12271
  }, {
11154
12272
  group: config,
11155
12273
  template: "seatAccepted",
11156
- to: text7(purchaser.email),
12274
+ to: text8(purchaser.email),
11157
12275
  dedupeKey: `named-seat-accepted:${input.seatId}`,
11158
12276
  applicationId: input.primaryApplicationId,
11159
12277
  tierId: input.skuId,
11160
12278
  vars: {
11161
- firstName: text7(purchaser.firstName),
12279
+ firstName: text8(purchaser.firstName),
11162
12280
  purchaserName,
11163
12281
  recipientName: input.recipientName,
11164
12282
  tierName: await tierName(db, group, input.skuId),
@@ -11619,8 +12737,8 @@ async function upcomingEvents(env) {
11619
12737
  const res = await cal.availability.upcoming();
11620
12738
  return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
11621
12739
  }
11622
- function toReconcile(rows5) {
11623
- return rows5.map((m) => ({
12740
+ function toReconcile(rows6) {
12741
+ return rows6.map((m) => ({
11624
12742
  id: String(m.id),
11625
12743
  applicationId: String(m.applicationId),
11626
12744
  googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
@@ -11642,12 +12760,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11642
12760
  const query = await db.query({
11643
12761
  meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
11644
12762
  });
11645
- let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
11646
- if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
11647
- 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);
11648
12766
  let decisions = [];
11649
12767
  try {
11650
- decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
12768
+ decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
11651
12769
  } catch {
11652
12770
  }
11653
12771
  const patched = /* @__PURE__ */ new Map();
@@ -11674,7 +12792,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11674
12792
  };
11675
12793
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
11676
12794
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
11677
- 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");
11678
12796
  return json({ meetings, adopted: decisions.length, timezone });
11679
12797
  };
11680
12798
 
@@ -11813,15 +12931,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11813
12931
  for (const a of data.applications ?? []) {
11814
12932
  const key3 = String(a.email ?? "").toLowerCase();
11815
12933
  if (!key3) continue;
11816
- const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
12934
+ const name2 = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
11817
12935
  const row2 = people.get(key3);
11818
12936
  if (row2) {
11819
12937
  if (!row2.application) row2.application = applicationSummary(a);
11820
- if (!row2.name) row2.name = name;
12938
+ if (!row2.name) row2.name = name2;
11821
12939
  } else {
11822
12940
  people.set(key3, {
11823
12941
  email: String(a.email),
11824
- name,
12942
+ name: name2,
11825
12943
  userId: null,
11826
12944
  role: null,
11827
12945
  profile: null,
@@ -11830,10 +12948,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11830
12948
  });
11831
12949
  }
11832
12950
  }
11833
- const rows5 = [...people.values()].sort(
12951
+ const rows6 = [...people.values()].sort(
11834
12952
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
11835
12953
  );
11836
- return json({ people: rows5 });
12954
+ return json({ people: rows6 });
11837
12955
  };
11838
12956
  var handleAdminPeopleAccess = async (req, url, env, ctx) => {
11839
12957
  if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
@@ -12034,7 +13152,7 @@ async function gate3(req, env, ctx) {
12034
13152
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
12035
13153
  return rawDb;
12036
13154
  }
12037
- var rows4 = (v) => Array.isArray(v) ? v : [];
13155
+ var rows5 = (v) => Array.isArray(v) ? v : [];
12038
13156
  var handleAdminDashboard = async (req, url, env, ctx) => {
12039
13157
  if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
12040
13158
  const got = await gate3(req, env, ctx);
@@ -12056,8 +13174,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12056
13174
  crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
12057
13175
  groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
12058
13176
  });
12059
- const apps = rows4(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
12060
- 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;
12061
13179
  const applications = {
12062
13180
  total: apps.length,
12063
13181
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -12073,7 +13191,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12073
13191
  for (const a of apps) {
12074
13192
  if (String(a.status) in pipeline) pipeline[String(a.status)] = (pipeline[String(a.status)] ?? 0) + 1;
12075
13193
  }
12076
- for (const r of rows4(data.crm_record)) {
13194
+ for (const r of rows5(data.crm_record)) {
12077
13195
  if (!appById.has(r.fields?.applicationId)) continue;
12078
13196
  const s = r.stage;
12079
13197
  if (s in pipeline) {
@@ -12081,7 +13199,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12081
13199
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
12082
13200
  }
12083
13201
  }
12084
- const meetings = rows4(data.meetings).filter((m) => appById.has(m.applicationId));
13202
+ const meetings = rows5(data.meetings).filter((m) => appById.has(m.applicationId));
12085
13203
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
12086
13204
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
12087
13205
  const agenda = upcoming.slice(0, 8).map((m) => {
@@ -12104,12 +13222,12 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12104
13222
  const sk = await getVaultSecret(db, "stripe_secret_key");
12105
13223
  if (sk) {
12106
13224
  const subsRes = await chapterBillingSubscriptions(sk, ctx.chapter.id, env).catch(() => null);
12107
- if (subsRes?.complete && rows4(data.applications).length < 1e3) {
13225
+ if (subsRes?.complete && rows5(data.applications).length < 1e3) {
12108
13226
  const subs = subsRes.objects.filter((s) => {
12109
13227
  const a = appById.get(s.metadata?.applicationId);
12110
13228
  return a?.stripeSubscriptionId === s.id && a?.stripeCustomerId === s.customer;
12111
13229
  });
12112
- 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]));
12113
13231
  const amount = (s) => chapterBillingAmount(s, intents.get(s.metadata?.applicationId), env);
12114
13232
  const subMs = (s) => (s.created ?? 0) * 1e3;
12115
13233
  membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
@@ -12148,9 +13266,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12148
13266
  const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
12149
13267
  if (!subsRes) return json({ error: "billing lookup failed upstream" }, 502);
12150
13268
  const subById = new Map(subsRes.objects.map((s) => [s.id, s]));
12151
- 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]));
12152
13270
  const billingRows = [];
12153
- for (const a of rows4(data.applications)) {
13271
+ for (const a of rows5(data.applications)) {
12154
13272
  if (!chapterBillingApplication(a, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
12155
13273
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
12156
13274
  const candidate = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
@@ -12186,9 +13304,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12186
13304
  return json({
12187
13305
  billingReady: true,
12188
13306
  testMode,
12189
- truncated: !subsRes.complete || rows4(data.applications).length >= 200,
13307
+ truncated: !subsRes.complete || rows5(data.applications).length >= 200,
12190
13308
  rows: billingRows,
12191
- summary: subsRes.complete && rows4(data.applications).length < 200 ? summary : null
13309
+ summary: subsRes.complete && rows5(data.applications).length < 200 ? summary : null
12192
13310
  });
12193
13311
  };
12194
13312
 
@@ -12580,7 +13698,7 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
12580
13698
  var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
12581
13699
  function emailSpamSignals(copy) {
12582
13700
  const subject = typeof copy.subject === "string" ? copy.subject : "";
12583
- const text10 = typeof copy.text === "string" ? copy.text : "";
13701
+ const text11 = typeof copy.text === "string" ? copy.text : "";
12584
13702
  const html = typeof copy.html === "string" ? copy.html : "";
12585
13703
  const out = [];
12586
13704
  const letters = strip(subject).replace(/[^A-Za-z]/g, "");
@@ -12590,11 +13708,11 @@ function emailSpamSignals(copy) {
12590
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." });
12591
13709
  const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
12592
13710
  if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
12593
- const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
13711
+ const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text11));
12594
13712
  if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
12595
- 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." });
12596
- if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
12597
- const urls = [...`${text10}
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}
12598
13716
  ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
12599
13717
  const hosts = urls.map((url) => {
12600
13718
  try {
@@ -12607,7 +13725,7 @@ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
12607
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." });
12608
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." });
12609
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." });
12610
- 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." });
12611
13729
  return out;
12612
13730
  }
12613
13731
  function emailSenderWarnings(input) {
@@ -12647,8 +13765,8 @@ function validateEmailReadiness(input) {
12647
13765
  ${template.text ?? ""}
12648
13766
  ${html}`;
12649
13767
  const sectionOnly = new Set(templateSectionNames2(copy));
12650
- for (const name of templateVars2(copy)) if (name in input.groupValues && !input.groupValues[name]?.trim() && !sectionOnly.has(name)) {
12651
- problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
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.` });
12652
13770
  }
12653
13771
  }
12654
13772
  if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
@@ -12717,20 +13835,20 @@ async function updateEmailSettings(db, ctx, env, body) {
12717
13835
  if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
12718
13836
  const templates = body.emailTemplates;
12719
13837
  if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
12720
- const clean = {};
13838
+ const clean2 = {};
12721
13839
  for (const [key3, value] of Object.entries(templates)) {
12722
13840
  if (!events[key3]) continue;
12723
13841
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12724
13842
  const t = value;
12725
13843
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12726
- const subject = str2(t.subject).trim(), text10 = str2(t.text), html = str2(t.html);
13844
+ const subject = str2(t.subject).trim(), text11 = str2(t.text), html = str2(t.html);
12727
13845
  if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
12728
- const issues = validateEmailCopy4({ subject, text: text10 }, { allowedVariables: events[key3].variables });
13846
+ const issues = validateEmailCopy4({ subject, text: text11 }, { allowedVariables: events[key3].variables });
12729
13847
  const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
12730
13848
  if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
12731
13849
  return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
12732
13850
  }
12733
- clean[key3] = { subject, text: text10, ...html.trim() ? { html } : {}, enabled: t.enabled };
13851
+ clean2[key3] = { subject, text: text11, ...html.trim() ? { html } : {}, enabled: t.enabled };
12734
13852
  }
12735
13853
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12736
13854
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -12743,15 +13861,15 @@ async function updateEmailSettings(db, ctx, env, body) {
12743
13861
  if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
12744
13862
  if (ctx.chapter.mode === "hub") {
12745
13863
  const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
12746
- 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 });
12747
13865
  } else {
12748
13866
  const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
12749
13867
  await db.transact([
12750
- { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
13868
+ { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean2 } },
12751
13869
  { t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
12752
13870
  ]);
12753
13871
  }
12754
- 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));
12755
13873
  return json({ ok: true, warnings });
12756
13874
  }
12757
13875
 
@@ -12789,8 +13907,8 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
12789
13907
  const got = await gate5(req, env, ctx);
12790
13908
  if (got instanceof Response) return got;
12791
13909
  if (ctx.chapter.mode === "hub") {
12792
- const rows5 = await listEmailLog({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
12793
- 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 })) });
12794
13912
  }
12795
13913
  const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
12796
13914
  const visible = [];
@@ -13020,7 +14138,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
13020
14138
  return json({
13021
14139
  targets: ctx.chapter.network.targets.map(({
13022
14140
  id: id7,
13023
- name,
14141
+ name: name2,
13024
14142
  url: targetUrl,
13025
14143
  fields,
13026
14144
  sharedNotes,
@@ -13029,7 +14147,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
13029
14147
  commercialParity
13030
14148
  }) => ({
13031
14149
  id: id7,
13032
- name,
14150
+ name: name2,
13033
14151
  url: targetUrl,
13034
14152
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
13035
14153
  sharedNotes,
@@ -13368,8 +14486,8 @@ function terms(value) {
13368
14486
  function matchesReaderIdentity(record4, search, includeEmail) {
13369
14487
  const query = terms(search);
13370
14488
  if (!query.length) return true;
13371
- const name = folded(record4.name);
13372
- 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)];
13373
14491
  if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
13374
14492
  return query.every((term) => visible.some((candidate) => candidate.includes(term)));
13375
14493
  }
@@ -13398,14 +14516,14 @@ async function listVisibleSearchPopulation(db, where, window, count) {
13398
14516
  })));
13399
14517
  return pages.flatMap((page) => (page[CRM_NS.record] ?? []).map(rowToRecord));
13400
14518
  }
13401
- async function listReaderRecords(db, type, allowed2, emailField, window) {
14519
+ async function listReaderRecords(db, type, allowed, emailField, window) {
13402
14520
  const where = baseWhere(type, window.stage);
13403
14521
  const crmDb = db;
13404
14522
  const aggregate = await crmDb.aggregate(CRM_NS.record, { count: true }, { where });
13405
14523
  const candidateCount = aggregate.count ?? 0;
13406
14524
  if (window.search) {
13407
14525
  const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
13408
- const includeEmail = Boolean(emailField && allowed2.includes(emailField));
14526
+ const includeEmail = Boolean(emailField && allowed.includes(emailField));
13409
14527
  const matches2 = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
13410
14528
  return {
13411
14529
  records: matches2.slice(window.offset, window.offset + window.limit),
@@ -13498,7 +14616,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13498
14616
  const editable = reader.editableFields[input.type];
13499
14617
  if (hasFields) {
13500
14618
  const names = Object.keys(fields);
13501
- 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))) {
13502
14620
  return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
13503
14621
  }
13504
14622
  } else if (!reader.stageTransitions.includes(input.type)) {
@@ -13563,7 +14681,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13563
14681
  });
13564
14682
  }
13565
14683
  const capabilities = await operationalReaderRecord(record4, reader, db, env, ctx);
13566
- 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))) {
13567
14685
  return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
13568
14686
  }
13569
14687
  const deps = { crm: ctx.chapter.crm, db };
@@ -13618,8 +14736,8 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13618
14736
  }
13619
14737
  }
13620
14738
  const type = url.searchParams.get("type") ?? "";
13621
- const allowed2 = reader.fields[type];
13622
- 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);
13623
14741
  const recordId = url.searchParams.get("recordId");
13624
14742
  if (recordId !== null) {
13625
14743
  if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
@@ -13647,7 +14765,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13647
14765
  page = await listReaderRecords(
13648
14766
  db,
13649
14767
  type,
13650
- allowed2,
14768
+ allowed,
13651
14769
  typeDef.emailField,
13652
14770
  window
13653
14771
  );
@@ -13728,8 +14846,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
13728
14846
  async function mutateTargetRecord(db, env, ctx, target, input) {
13729
14847
  const fields = input.fields;
13730
14848
  if (fields) {
13731
- const allowed2 = target.editableFields[input.type];
13732
- 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))) {
13733
14851
  return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
13734
14852
  }
13735
14853
  } else if (!target.stageTransitions.includes(input.type)) {
@@ -14577,20 +15695,20 @@ async function networkPersonBulkSha256(value) {
14577
15695
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
14578
15696
  return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
14579
15697
  }
14580
- function assertNetworkPersonBulkRows(rows5) {
14581
- if (!Array.isArray(rows5) || rows5.length < 1) {
15698
+ function assertNetworkPersonBulkRows(rows6) {
15699
+ if (!Array.isArray(rows6) || rows6.length < 1) {
14582
15700
  throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
14583
15701
  }
14584
- if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
15702
+ if (rows6.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
14585
15703
  throw new NetworkPersonRegistryError(
14586
15704
  "bulk_rows_exceeded",
14587
15705
  `bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
14588
15706
  );
14589
15707
  }
14590
- return rows5;
15708
+ return rows6;
14591
15709
  }
14592
- async function networkPersonBulkBatchDigest(rows5) {
14593
- const bounded = assertNetworkPersonBulkRows(rows5);
15710
+ async function networkPersonBulkBatchDigest(rows6) {
15711
+ const bounded = assertNetworkPersonBulkRows(rows6);
14594
15712
  return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
14595
15713
  }
14596
15714
  async function networkPersonRegistryStateDigest(state) {
@@ -14605,7 +15723,7 @@ async function networkPersonRegistryStateDigest(state) {
14605
15723
  function rejectedValidation(errors) {
14606
15724
  return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
14607
15725
  }
14608
- async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
15726
+ async function classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state) {
14609
15727
  const aliasIds = new Set(state.aliases.map((alias) => alias.id));
14610
15728
  const batchAliases = /* @__PURE__ */ new Set();
14611
15729
  const claimOwners = /* @__PURE__ */ new Map();
@@ -14616,8 +15734,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14616
15734
  }
14617
15735
  const results = [];
14618
15736
  const values = [];
14619
- for (let index2 = 0; index2 < rows5.length; index2 += 1) {
14620
- const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
15737
+ for (let index2 = 0; index2 < rows6.length; index2 += 1) {
15738
+ const validation = validateNetworkPersonImport(rows6[index2], allowedChapterIds);
14621
15739
  if (!validation.ok || !validation.value) {
14622
15740
  const errors = validation.errors.slice(0, 8);
14623
15741
  results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
@@ -14650,8 +15768,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14650
15768
  }
14651
15769
  return { rows: results, values };
14652
15770
  }
14653
- async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
14654
- const bounded = assertNetworkPersonBulkRows(rows5);
15771
+ async function previewNetworkPersonBulkImport(db, rows6, allowedChapterIds) {
15772
+ const bounded = assertNetworkPersonBulkRows(rows6);
14655
15773
  const state = await loadNetworkPersonRegistry(db);
14656
15774
  const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
14657
15775
  const counts = networkPersonBulkEmptyCounts();
@@ -14682,8 +15800,8 @@ function eventCounts(event2) {
14682
15800
  }
14683
15801
  async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
14684
15802
  if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
14685
- const rows5 = assertNetworkPersonBulkRows(input.rows);
14686
- const batchDigest = await networkPersonBulkBatchDigest(rows5);
15803
+ const rows6 = assertNetworkPersonBulkRows(input.rows);
15804
+ const batchDigest = await networkPersonBulkBatchDigest(rows6);
14687
15805
  if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
14688
15806
  throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
14689
15807
  }
@@ -14702,7 +15820,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14702
15820
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14703
15821
  batchDigest,
14704
15822
  stateDigest: eventString(completed, "stateDigest"),
14705
- rowCount: Number(completed.detail.rowCount) || rows5.length,
15823
+ rowCount: Number(completed.detail.rowCount) || rows6.length,
14706
15824
  counts: eventCounts(completed),
14707
15825
  rows: [],
14708
15826
  replayed: true
@@ -14710,7 +15828,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14710
15828
  }
14711
15829
  let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
14712
15830
  if (!started) {
14713
- const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
15831
+ const preview = await previewNetworkPersonBulkImport(db, rows6, allowedChapterIds);
14714
15832
  if (preview.stateDigest !== input.expectedStateDigest) {
14715
15833
  throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
14716
15834
  }
@@ -14719,7 +15837,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14719
15837
  networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
14720
15838
  batchDigest,
14721
15839
  stateDigest: input.expectedStateDigest,
14722
- rowCount: rows5.length,
15840
+ rowCount: rows6.length,
14723
15841
  previewCounts: preview.counts
14724
15842
  })
14725
15843
  ], {
@@ -14735,7 +15853,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14735
15853
  if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
14736
15854
  throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
14737
15855
  }
14738
- const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
15856
+ const classified = await classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state);
14739
15857
  const results = [];
14740
15858
  const counts = networkPersonBulkEmptyCounts();
14741
15859
  for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
@@ -14774,10 +15892,10 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14774
15892
  }
14775
15893
  if (counts.failed === 0) {
14776
15894
  await db.transact([
14777
- networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
15895
+ networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows6.length + 1, {
14778
15896
  batchDigest,
14779
15897
  stateDigest: input.expectedStateDigest,
14780
- rowCount: rows5.length,
15898
+ rowCount: rows6.length,
14781
15899
  counts
14782
15900
  })
14783
15901
  ], { mutationId: completedMutationId });
@@ -14786,7 +15904,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14786
15904
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14787
15905
  batchDigest,
14788
15906
  stateDigest: input.expectedStateDigest,
14789
- rowCount: rows5.length,
15907
+ rowCount: rows6.length,
14790
15908
  counts,
14791
15909
  rows: results
14792
15910
  };
@@ -14907,7 +16025,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
14907
16025
  }
14908
16026
 
14909
16027
  // src/worker-routes-network-person-registry.ts
14910
- var PATH6 = "/api/admin/network-person-registry";
16028
+ var PATH7 = "/api/admin/network-person-registry";
14911
16029
  var MAX_BODY2 = 8 * 1024 * 1024;
14912
16030
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
14913
16031
  var positiveRevision = (value) => {
@@ -14932,7 +16050,7 @@ function registryError(error) {
14932
16050
  return null;
14933
16051
  }
14934
16052
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14935
- if (url.pathname !== PATH6) return null;
16053
+ if (url.pathname !== PATH7) return null;
14936
16054
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
14937
16055
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
14938
16056
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -14944,9 +16062,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14944
16062
  if (req.method === "POST") {
14945
16063
  let body;
14946
16064
  try {
14947
- const text10 = await req.text();
14948
- if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14949
- 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);
14950
16068
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14951
16069
  body = parsed;
14952
16070
  } catch {
@@ -15115,7 +16233,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
15115
16233
  // src/network-commercial-parity.ts
15116
16234
  var DIGEST = /^[0-9a-f]{64}$/;
15117
16235
  var MAX_RECORDS = 500;
15118
- 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;
15119
16237
  function parityMessage(partition, kind, value) {
15120
16238
  return [
15121
16239
  "chapter-commercial-parity",
@@ -15147,18 +16265,18 @@ async function digestWithKey(key3, partition, kind, value) {
15147
16265
  async function buildChapterCommercialParitySnapshot(input) {
15148
16266
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
15149
16267
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
15150
- 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)));
15151
16269
  const selected2 = eligible.slice(0, maxRecords);
15152
16270
  const key3 = await parityKey(input.secret);
15153
16271
  const records = await Promise.all(selected2.map(async (row2) => {
15154
- const applicationId = text8(row2.id);
15155
- const customerId = text8(row2.stripeCustomerId);
15156
- const subscriptionId = text8(row2.stripeSubscriptionId);
16272
+ const applicationId = text9(row2.id);
16273
+ const customerId = text9(row2.stripeCustomerId);
16274
+ const subscriptionId = text9(row2.stripeSubscriptionId);
15157
16275
  return {
15158
16276
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
15159
16277
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
15160
16278
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
15161
- status: text8(row2.status, 64),
16279
+ status: text9(row2.status, 64),
15162
16280
  canceled: row2.canceled === true
15163
16281
  };
15164
16282
  }));
@@ -15184,7 +16302,7 @@ function validSnapshot(snapshot, source) {
15184
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;
15185
16303
  const seen = /* @__PURE__ */ new Set();
15186
16304
  for (const record4 of records) {
15187
- 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;
15188
16306
  const key3 = recordKey(record4);
15189
16307
  if (seen.has(key3)) return false;
15190
16308
  seen.add(key3);
@@ -15195,16 +16313,16 @@ async function reconcileCommercialParitySnapshot(input) {
15195
16313
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
15196
16314
  const sourceKey = networkCommercialSourceKey(input.source);
15197
16315
  const key3 = await parityKey(input.secret);
15198
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
15199
- const customerId = text8(row2.customerId);
15200
- 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);
15201
16319
  return {
15202
16320
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
15203
16321
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
15204
16322
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
15205
16323
  status: null,
15206
16324
  canceled: false,
15207
- stripeStatus: text8(row2.status, 64),
16325
+ stripeStatus: text9(row2.status, 64),
15208
16326
  stripeCanceled: row2.status === "canceled"
15209
16327
  };
15210
16328
  }));
@@ -15286,14 +16404,14 @@ var PAGE2 = 500;
15286
16404
  var MAX_SCAN = 5e3;
15287
16405
  var MAX_RECORDS2 = 500;
15288
16406
  async function localCommercialRows(db) {
15289
- const rows5 = [];
16407
+ const rows6 = [];
15290
16408
  for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
15291
16409
  const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
15292
16410
  const page = Array.isArray(result.applications) ? result.applications : [];
15293
- rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
15294
- 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 };
15295
16413
  }
15296
- return { rows: rows5, truncated: true };
16414
+ return { rows: rows6, truncated: true };
15297
16415
  }
15298
16416
  var handleNetworkCommercialParity = async (req, url, env, ctx) => {
15299
16417
  if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
@@ -15428,8 +16546,8 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15428
16546
  const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
15429
16547
  return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
15430
16548
  }
15431
- const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15432
- 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));
15433
16551
  if (!member) return { disposition: "not_applicable" };
15434
16552
  const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
15435
16553
  return { disposition: "applied", membershipId: receipt.membershipId };
@@ -15673,15 +16791,15 @@ var handleFormation = async (req, url, env, ctx) => {
15673
16791
  if (submissionId && !SUBMISSION.test(submissionId)) {
15674
16792
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
15675
16793
  }
15676
- const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
15677
- 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));
15678
16796
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
15679
16797
  for (const field of formation.required) {
15680
16798
  const value = body[field];
15681
16799
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
15682
16800
  }
15683
16801
  const input = {};
15684
- for (const field of allowed2) {
16802
+ for (const field of allowed) {
15685
16803
  const value = body[field];
15686
16804
  if (value === void 0) continue;
15687
16805
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -15745,10 +16863,10 @@ var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
15745
16863
  var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
15746
16864
  var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
15747
16865
  var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
15748
- function unknownKeys3(value, allowed2, path, errors) {
15749
- 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`);
15750
16868
  }
15751
- function text9(value, path, errors, max = MAX_TEXT2, required = true) {
16869
+ function text10(value, path, errors, max = MAX_TEXT2, required = true) {
15752
16870
  if (value === void 0 && !required) return void 0;
15753
16871
  if (typeof value !== "string" || required && !value.trim()) {
15754
16872
  errors.push(`${path} must be a non-empty string`);
@@ -15774,7 +16892,7 @@ function scopeValue(value, path, errors) {
15774
16892
  return { kind: raw2.kind };
15775
16893
  }
15776
16894
  if (raw2.kind === "chapter") {
15777
- const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
16895
+ const chapterId = text10(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15778
16896
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15779
16897
  return { kind: "chapter", chapterId };
15780
16898
  }
@@ -15788,22 +16906,22 @@ function resourceValue(value, path, errors) {
15788
16906
  return null;
15789
16907
  }
15790
16908
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15791
- const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
16909
+ const id7 = text10(raw2.id, `${path}.id`, errors, 80) ?? "";
15792
16910
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15793
- const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
16911
+ const kind = text10(raw2.kind, `${path}.kind`, errors) ?? "";
15794
16912
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15795
- const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
15796
- const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
15797
- 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) ?? "";
15798
16916
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15799
16917
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15800
16918
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15801
- 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) ?? "") : [];
15802
16920
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15803
16921
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15804
16922
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15805
- const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15806
- 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);
15807
16925
  if (url) {
15808
16926
  try {
15809
16927
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15815,9 +16933,9 @@ function resourceValue(value, path, errors) {
15815
16933
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15816
16934
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15817
16935
  const brandAsset = asset ? {
15818
- bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15819
- assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15820
- ...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) ?? "" }
15821
16939
  } : void 0;
15822
16940
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15823
16941
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15832,17 +16950,17 @@ function resourceValue(value, path, errors) {
15832
16950
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15833
16951
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15834
16952
  const promotedFrom = promotedRaw ? {
15835
- chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15836
- 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) ?? "",
15837
16955
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15838
16956
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15839
- promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
16957
+ promotedBy: text10(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15840
16958
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15841
16959
  } : void 0;
15842
16960
  const provenance2 = {
15843
- createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
16961
+ createdBy: text10(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15844
16962
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15845
- updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
16963
+ updatedBy: text10(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15846
16964
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15847
16965
  ...promotedFrom ? { promotedFrom } : {}
15848
16966
  };
@@ -15861,7 +16979,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15861
16979
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15862
16980
  const sourceType = sourceRaw?.type;
15863
16981
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15864
- const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
16982
+ const sourceAppId = text10(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15865
16983
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15866
16984
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15867
16985
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15869,8 +16987,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15869
16987
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15870
16988
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15871
16989
  const target = targetRaw ? {
15872
- appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15873
- 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) ?? ""
15874
16992
  } : void 0;
15875
16993
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15876
16994
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15958,17 +17076,17 @@ async function builderLibraryDigest(document) {
15958
17076
  var control2 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
15959
17077
  var local3 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
15960
17078
  async function loadActiveBuilderLibraryControls(db, targetId, environment) {
15961
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
15962
- 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) : [];
15963
17081
  }
15964
17082
  async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
15965
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
15966
- 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);
15967
17085
  }
15968
17086
  async function loadLocalBuilderLibrary(db, appId, environment) {
15969
17087
  const id7 = `builder-library-local:${environment}`;
15970
- const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
15971
- 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);
15972
17090
  }
15973
17091
  async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
15974
17092
  const { document, digest } = envelope;
@@ -17611,8 +18729,8 @@ var readable = /* @__PURE__ */ new Set([
17611
18729
  "networkCommercialEvents",
17612
18730
  "networkCommercialExceptions"
17613
18731
  ]);
17614
- var check = (ns, allowed2) => {
17615
- 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");
17616
18734
  return physical(ns);
17617
18735
  };
17618
18736
  function createMembershipAuthorityStorage(db, secretNames) {
@@ -17638,9 +18756,9 @@ function createMembershipAuthorityStorage(db, secretNames) {
17638
18756
  const guards = opts?.guards?.map((guard) => ({ ...guard, ns: check(guard.ns, readable) }));
17639
18757
  return db.transact(mapped, { ...opts, ...guards ? { guards } : {} });
17640
18758
  },
17641
- secrets: { get(name) {
17642
- if (!secrets.has(name)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
17643
- 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);
17644
18762
  } }
17645
18763
  };
17646
18764
  }
@@ -17810,7 +18928,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17810
18928
  const origin = new URL(options.origin), limit = options.limit ?? 100;
17811
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");
17812
18930
  const db = ctx.makeDb(env);
17813
- const rows5 = (await db.query({ applications: { $: {
18931
+ const rows6 = (await db.query({ applications: { $: {
17814
18932
  where: {
17815
18933
  groupId: ctx.chapter.id,
17816
18934
  stripeRuntime: env.ODLA_RUNTIME ?? "default",
@@ -17821,7 +18939,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17821
18939
  limit: limit + 1
17822
18940
  } } })).applications ?? [];
17823
18941
  let repaired = 0, pending = 0;
17824
- for (const app of rows5.slice(0, limit)) {
18942
+ for (const app of rows6.slice(0, limit)) {
17825
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");
17826
18944
  try {
17827
18945
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { approvalEffectsLastAttemptAt: Date.now() } }], {
@@ -17846,7 +18964,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17846
18964
  pending++;
17847
18965
  }
17848
18966
  }
17849
- return { repaired, pending, more: rows5.length > limit };
18967
+ return { repaired, pending, more: rows6.length > limit };
17850
18968
  }
17851
18969
 
17852
18970
  // src/membership-chapter-delivery.ts
@@ -17862,7 +18980,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17862
18980
  };
17863
18981
  signal.addEventListener("abort", cancel, { once: true });
17864
18982
  let size = 0;
17865
- let text10 = "";
18983
+ let text11 = "";
17866
18984
  const decoder = new TextDecoder();
17867
18985
  try {
17868
18986
  while (true) {
@@ -17872,10 +18990,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17872
18990
  if (done) break;
17873
18991
  size += value.byteLength;
17874
18992
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17875
- text10 += decoder.decode(value, { stream: true });
18993
+ text11 += decoder.decode(value, { stream: true });
17876
18994
  }
17877
18995
  try {
17878
- return JSON.parse(text10 + decoder.decode());
18996
+ return JSON.parse(text11 + decoder.decode());
17879
18997
  } catch {
17880
18998
  throw new TypeError(`invalid ${label}`);
17881
18999
  }
@@ -17894,8 +19012,8 @@ import { validateEmailCopy as validateEmailCopy5 } from "@odla-ai/crm";
17894
19012
  function invitationDelivery(env, recipientEmail) {
17895
19013
  const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
17896
19014
  if (env.ODLA_ENV !== "dev" || !env.SEND_INVITATION_TEST) return normal;
17897
- const allowed2 = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
17898
- 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;
17899
19017
  const to = env.ODLA_INVITATION_TEST_TO?.trim();
17900
19018
  const from = env.ODLA_INVITATION_TEST_FROM?.trim();
17901
19019
  const valid = (value) => value && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(value);
@@ -18415,27 +19533,27 @@ function validatedTokens(input) {
18415
19533
  }
18416
19534
  const tokens = {};
18417
19535
  let serializedLength = 0;
18418
- for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
18419
- if (!TOKEN_NAME.test(name)) {
18420
- 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`);
18421
19539
  }
18422
19540
  if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
18423
- 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`);
18424
19542
  }
18425
19543
  const value = String(raw2).trim();
18426
19544
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
18427
- 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`);
18428
19546
  }
18429
- serializedLength += name.length + value.length + 4;
19547
+ serializedLength += name2.length + value.length + 4;
18430
19548
  if (serializedLength > MAX_SERIALIZED_LENGTH) {
18431
19549
  throw new TypeError("signup-control theme exceeds the serialized size bound");
18432
19550
  }
18433
- tokens[name] = value;
19551
+ tokens[name2] = value;
18434
19552
  }
18435
19553
  return tokens;
18436
19554
  }
18437
19555
  function cssFor(tokens) {
18438
- const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
19556
+ const declarations = Object.entries(tokens).map(([name2, value]) => ` ${name2}: ${value};`);
18439
19557
  return declarations.length ? `:root {
18440
19558
  ${declarations.join("\n")}
18441
19559
  }` : "";
@@ -18550,6 +19668,7 @@ var BUILTIN_ROUTES = [
18550
19668
  handleFormation,
18551
19669
  handleMember,
18552
19670
  handleMemberContent,
19671
+ handleMemberPortal,
18553
19672
  handleGifts,
18554
19673
  handleNamedSeat,
18555
19674
  handleNamedSeatRegift,