@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.
@@ -4,8 +4,8 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
6
  var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
9
  };
10
10
  var __copyProps = (to, from, except, desc) => {
11
11
  if (from && typeof from === "object" || typeof from === "function") {
@@ -70,6 +70,20 @@ __export(worker_exports, {
70
70
  });
71
71
  module.exports = __toCommonJS(worker_exports);
72
72
 
73
+ // src/admin-second-factor.ts
74
+ function sessionHasSecondFactor(payload) {
75
+ const fva = payload?.fva;
76
+ return Array.isArray(fva) && typeof fva[1] === "number" && fva[1] >= 0;
77
+ }
78
+ function adminSecondFactorAllows(mode, payload, who) {
79
+ if (mode === "off" || sessionHasSecondFactor(payload)) return true;
80
+ if (mode === "report") {
81
+ console.warn(JSON.stringify({ event: "chapter.admin_second_factor_missing", mode, who: who ?? null }));
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+
73
87
  // src/worker-context.ts
74
88
  var import_db = require("@odla-ai/db");
75
89
  var import_jose = require("jose");
@@ -99,9 +113,9 @@ function canChangeRole(ctx) {
99
113
  }
100
114
  return { ok: true };
101
115
  }
102
- async function getVaultSecret(db, name) {
116
+ async function getVaultSecret(db, name2) {
103
117
  try {
104
- const value = await db.secrets.get(name);
118
+ const value = await db.secrets.get(name2);
105
119
  return typeof value === "string" && value !== "" ? value : void 0;
106
120
  } catch {
107
121
  return void 0;
@@ -434,7 +448,8 @@ function createWorkerContext(options) {
434
448
  return await isAdminEmail(db, u.email) ? auth.adminRole : auth.ladder[0];
435
449
  }
436
450
  async function isAdmin(db, u) {
437
- return await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
451
+ const admin = await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
452
+ return admin && adminSecondFactorAllows(auth.adminSecondFactor ?? "off", u.payload, u.email ?? u.userId);
438
453
  }
439
454
  function crmSender(env) {
440
455
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return void 0;
@@ -922,14 +937,14 @@ function projectNetworkCommercialEvents(input, sources) {
922
937
  const prior = latest.get(key3);
923
938
  if (!prior || order(prior, fact) <= 0) latest.set(key3, row2);
924
939
  }
925
- const rows5 = [...latest.values()];
926
- const customers = rows5.filter((row2) => row2.objectKind === "customer");
927
- const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
928
- const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
929
- const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
930
- const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
931
- const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
932
- for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
940
+ const rows6 = [...latest.values()];
941
+ const customers = rows6.filter((row2) => row2.objectKind === "customer");
942
+ const subscriptions = rows6.filter((row2) => row2.objectKind === "subscription");
943
+ const invoices = rows6.filter((row2) => row2.objectKind === "invoice");
944
+ const seatPayments = rows6.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
945
+ const disputes = rows6.filter((row2) => row2.objectKind === "dispute");
946
+ const refunds = rows6.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
947
+ for (const charge of rows6.filter((row2) => row2.eventType === "charge.refunded")) {
933
948
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
934
949
  exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
935
950
  }
@@ -1187,11 +1202,11 @@ async function readStripeRecoveryEvent(db, eventId) {
1187
1202
  }
1188
1203
  async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
1189
1204
  const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
1190
- const rows5 = await db.query({
1205
+ const rows6 = await db.query({
1191
1206
  networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
1192
1207
  networkCommercialExceptions: { $: { where: { id: exceptionId2 }, limit: 1 } }
1193
1208
  });
1194
- const stored = rows5.networkCommercialEvents?.[0], exception = rows5.networkCommercialExceptions?.[0];
1209
+ const stored = rows6.networkCommercialEvents?.[0], exception = rows6.networkCommercialExceptions?.[0];
1195
1210
  if (!stored) throw new Error("stored event not found");
1196
1211
  const provider = await readStripeRecoveryEvent(db, eventId);
1197
1212
  const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
@@ -1267,7 +1282,7 @@ function findGiftRef(obj) {
1267
1282
  const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
1268
1283
  const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
1269
1284
  const candidates = [metaOf(obj), metaOf(subscription)];
1270
- const pickIdentity = (name) => candidates.map((metadata) => typeof metadata[name] === "string" ? String(metadata[name]) : void 0).find(Boolean);
1285
+ const pickIdentity = (name2) => candidates.map((metadata) => typeof metadata[name2] === "string" ? String(metadata[name2]) : void 0).find(Boolean);
1271
1286
  const appId = pickIdentity("odlaAppId");
1272
1287
  const environment = pickIdentity("odlaEnvironment");
1273
1288
  const dataEnvironment = pickIdentity("odlaDataEnvironment");
@@ -1364,11 +1379,11 @@ async function archivedChapterMembershipEvent(db, rawBody, app, env, ctx) {
1364
1379
  if (!app.membershipRestartId) return false;
1365
1380
  const fact = normalizeNetworkCommercialStripeEvent(JSON.parse(rawBody), "");
1366
1381
  if (!fact.subscriptionId || fact.subscriptionId === app.stripeSubscriptionId) return false;
1367
- const rows5 = (await db.query({ membershipCheckoutHistory: { $: { where: {
1382
+ const rows6 = (await db.query({ membershipCheckoutHistory: { $: { where: {
1368
1383
  applicationId: app.id,
1369
1384
  providerSubscriptionId: fact.subscriptionId
1370
1385
  }, limit: 2 } } })).membershipCheckoutHistory ?? [];
1371
- return rows5.length === 1 && rows5[0].appId === env.ODLA_APP_ID && rows5[0].chapterId === ctx.chapter.id && rows5[0].environment === env.ODLA_ENV && rows5[0].runtime === stripeRuntimeIdentity(env.ODLA_RUNTIME);
1386
+ return rows6.length === 1 && rows6[0].appId === env.ODLA_APP_ID && rows6[0].chapterId === ctx.chapter.id && rows6[0].environment === env.ODLA_ENV && rows6[0].runtime === stripeRuntimeIdentity(env.ODLA_RUNTIME);
1372
1387
  }
1373
1388
 
1374
1389
  // src/crm-sync.ts
@@ -1559,10 +1574,10 @@ async function findSharedRecord(deps, record4, tag) {
1559
1574
  if (found.crm_record?.[0]) return found.crm_record[0];
1560
1575
  }
1561
1576
  const nameField = def.nameField ?? "name";
1562
- const name = record4.input[nameField];
1563
- if (record4.type === "company" && typeof name === "string" && name.trim()) {
1577
+ const name2 = record4.input[nameField];
1578
+ if (record4.type === "company" && typeof name2 === "string" && name2.trim()) {
1564
1579
  const found = await deps.db.query({
1565
- crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
1580
+ crm_record: { $: { where: { type: record4.type, name: name2.trim() }, limit: 1 } }
1566
1581
  });
1567
1582
  if (found.crm_record?.[0]) return found.crm_record[0];
1568
1583
  }
@@ -1700,9 +1715,9 @@ async function backfillCrm(deps) {
1700
1715
  }
1701
1716
 
1702
1717
  // src/email-retirement.ts
1703
- function isRetiredEmailCopy(subject, text10) {
1718
+ function isRetiredEmailCopy(subject, text11) {
1704
1719
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1705
- const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1720
+ const bodyCopy = text11.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1706
1721
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1707
1722
  }
1708
1723
 
@@ -1727,8 +1742,8 @@ function lineHtml(line) {
1727
1742
  }
1728
1743
  return out + escapeHtml(line.slice(last));
1729
1744
  }
1730
- function textToHtml(text10) {
1731
- const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1745
+ function textToHtml(text11) {
1746
+ const paragraphs = text11.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
1732
1747
  const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
1733
1748
  return `<!doctype html>
1734
1749
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
@@ -1743,7 +1758,7 @@ function render(template, vars) {
1743
1758
  }
1744
1759
  var HTML_BODY_LIMIT = 2e5;
1745
1760
  function renderHtmlBody(template, vars) {
1746
- const escaped = Object.fromEntries(Object.entries(vars).map(([name, value]) => [name, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
1761
+ const escaped = Object.fromEntries(Object.entries(vars).map(([name2, value]) => [name2, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
1747
1762
  return (0, import_crm5.renderTemplate)(template, escaped);
1748
1763
  }
1749
1764
  function htmlBodyIssues(html, options) {
@@ -1753,7 +1768,7 @@ function htmlBodyIssues(html, options) {
1753
1768
  const unknown = new Set((0, import_crm5.templateVars)(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
1754
1769
  for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
1755
1770
  if (options.values) {
1756
- for (const name of (0, import_crm5.templateSectionNames)(html)) if (typeof options.values[name] !== "string") issues.push({ code: "value-missing", variable: name, message: `Missing event value: ${name} (HTML body).` });
1771
+ for (const name2 of (0, import_crm5.templateSectionNames)(html)) if (typeof options.values[name2] !== "string") issues.push({ code: "value-missing", variable: name2, message: `Missing event value: ${name2} (HTML body).` });
1757
1772
  for (const variable of (0, import_crm5.templateLiveVars)(html, options.values)) {
1758
1773
  if (unknown.has(variable)) continue;
1759
1774
  const value = options.values[variable];
@@ -1833,12 +1848,12 @@ function planDelivery(input) {
1833
1848
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1834
1849
  if ((0, import_crm6.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1835
1850
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1836
- const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
1851
+ const text11 = redirect ? `(dev redirect; original recipient: ${input.to})
1837
1852
 
1838
1853
  ` + renderedText : renderedText;
1839
1854
  const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
1840
1855
  const htmlOut = renderedHtml && redirect ? `<p style="font:13px monospace;color:#555;margin:0 0 12px">(dev redirect; original recipient: ${escapeHtml(input.to)})</p>` + renderedHtml : renderedHtml;
1841
- return { deliver: true, transport, to, subject, text: text10, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
1856
+ return { deliver: true, transport, to, subject, text: text11, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
1842
1857
  }
1843
1858
 
1844
1859
  // src/email-deliverability.ts
@@ -1874,9 +1889,9 @@ function tierPayable(tier, group, hasSecretKey) {
1874
1889
  function byOrder(a, b) {
1875
1890
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1876
1891
  }
1877
- function resolveTiers(group, rows5 = []) {
1878
- const stored = rows5.filter((tier) => tier.active);
1879
- if (rows5.length) return [...stored].sort(byOrder);
1892
+ function resolveTiers(group, rows6 = []) {
1893
+ const stored = rows6.filter((tier) => tier.active);
1894
+ if (rows6.length) return [...stored].sort(byOrder);
1880
1895
  return [{
1881
1896
  id: "standard",
1882
1897
  groupId: String(group.id ?? ""),
@@ -1932,16 +1947,16 @@ function rowToTier(row2, groupId) {
1932
1947
  }
1933
1948
  async function loadTiers(db, group) {
1934
1949
  const groupId = String(group.id ?? "");
1935
- let rows5;
1950
+ let rows6;
1936
1951
  try {
1937
- rows5 = (await db.query({
1952
+ rows6 = (await db.query({
1938
1953
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
1939
1954
  })).tiers;
1940
1955
  } catch {
1941
1956
  throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
1942
1957
  }
1943
- if (!Array.isArray(rows5)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1944
- const list = rows5.map((row2) => rowToTier(row2, groupId));
1958
+ if (!Array.isArray(rows6)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
1959
+ const list = rows6.map((row2) => rowToTier(row2, groupId));
1945
1960
  return resolveTiers(group, list);
1946
1961
  }
1947
1962
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -2006,8 +2021,8 @@ function formatInstant(at2, timeZone) {
2006
2021
  var adminUrlFor = (origin) => `${origin}/admin/`;
2007
2022
  var membersUrlFor = (origin) => `${origin}/members/`;
2008
2023
  function applicantVariables(app) {
2009
- const text10 = (v) => typeof v === "string" ? v : "";
2010
- return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
2024
+ const text11 = (v) => typeof v === "string" ? v : "";
2025
+ return { firstName: text11(app.firstName), lastName: text11(app.lastName), email: text11(app.email), phone: text11(app.phone), state: text11(app.state) };
2011
2026
  }
2012
2027
  function sampleVariables(origin, timeZone, now = Date.now()) {
2013
2028
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -2053,8 +2068,8 @@ async function emailTierVariables(db, input) {
2053
2068
  if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
2054
2069
  const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
2055
2070
  if (!tierId) return { tierName: "", tier: "" };
2056
- const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
2057
- const tierName2 = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
2071
+ const name2 = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
2072
+ const tierName2 = (0, import_crm7.isEmailTierName)(name2, tierId) ? name2.trim() : "";
2058
2073
  return { tierName: tierName2, tier: tierName2, tierId };
2059
2074
  } catch {
2060
2075
  return { tierName: "", tier: "" };
@@ -2062,8 +2077,8 @@ async function emailTierVariables(db, input) {
2062
2077
  }
2063
2078
 
2064
2079
  // src/notify.ts
2065
- function fromAddress(configured, name) {
2066
- const display = name?.trim();
2080
+ function fromAddress(configured, name2) {
2081
+ const display = name2?.trim();
2067
2082
  if (!display) return configured;
2068
2083
  const angled = configured.match(/<([^>]+)>/);
2069
2084
  return { name: display, email: (angled ? angled[1] : configured).trim() };
@@ -2134,14 +2149,14 @@ async function sendTemplated(deps, input) {
2134
2149
  if (decision.transport === "log-only") return { sent: true };
2135
2150
  let messageId;
2136
2151
  try {
2137
- const text10 = String(row2.body);
2138
- const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text10);
2152
+ const text11 = String(row2.body);
2153
+ const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text11);
2139
2154
  const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
2140
2155
  const response = await deps.sender.send({
2141
2156
  from: fromAddress(deps.from, input.group.fromName),
2142
2157
  to: [decision.to],
2143
2158
  subject: String(row2.subject),
2144
- text: text10,
2159
+ text: text11,
2145
2160
  ...html ? { html } : {},
2146
2161
  ...headers ? { headers } : {},
2147
2162
  replyTo: input.group.replyTo
@@ -2421,13 +2436,13 @@ async function authoritativeState(db, input) {
2421
2436
  return state;
2422
2437
  }
2423
2438
  async function currentPolicy(db, input, restartAware, state) {
2424
- const rows5 = await db.query({
2439
+ const rows6 = await db.query({
2425
2440
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
2426
2441
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
2427
2442
  });
2428
- if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2429
- 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));
2430
- const policies = rows5.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
2443
+ if (!Array.isArray(rows6.membershipOfferingVersion) || !Array.isArray(rows6.discountPolicyVersion) || rows6.membershipOfferingVersion.length >= 200 || rows6.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
2444
+ const offerings = rows6.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
2445
+ const policies = rows6.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
2431
2446
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
2432
2447
  const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
2433
2448
  const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
@@ -2545,13 +2560,13 @@ async function membershipRestartPayment(db, target, member, now) {
2545
2560
  // src/checkout-named-seat.ts
2546
2561
  function checkoutNamedSeatSelection(value, purchaserEmail) {
2547
2562
  const row2 = value;
2548
- const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
2563
+ const name2 = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
2549
2564
  const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
2550
2565
  const purchaser = normalizeChapterEmail(purchaserEmail);
2551
- if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
2566
+ if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
2552
2567
  throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
2553
2568
  }
2554
- return { recipientName: name, recipientEmail: email };
2569
+ return { recipientName: name2, recipientEmail: email };
2555
2570
  }
2556
2571
  async function checkoutNamedSeatIdentity(target, email) {
2557
2572
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -2992,14 +3007,14 @@ async function readMembershipAccess(db, input, now) {
2992
3007
  const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
2993
3008
  requireMembershipScope(entitlement, input);
2994
3009
  const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
2995
- const allowed2 = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
3010
+ const allowed = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
2996
3011
  return {
2997
3012
  membershipId: id7,
2998
- allowed: allowed2,
3013
+ allowed,
2999
3014
  status: String(member.status),
3000
3015
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
3001
3016
  revision: Number(member.revision),
3002
- restartEligible: !allowed2 && membershipCanRestart(member, now)
3017
+ restartEligible: !allowed && membershipCanRestart(member, now)
3003
3018
  };
3004
3019
  }
3005
3020
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -3144,11 +3159,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
3144
3159
  if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
3145
3160
  throw new Error("Stripe returned an invalid commercial list response");
3146
3161
  }
3147
- const rows5 = page.data.map(asRaw).filter((row2) => row2 !== null);
3148
- if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3149
- objects.push(...rows5);
3162
+ const rows6 = page.data.map(asRaw).filter((row2) => row2 !== null);
3163
+ if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
3164
+ objects.push(...rows6);
3150
3165
  if (!page.has_more) return { objects, complete: true };
3151
- const cursor = rows5.at(-1)?.id;
3166
+ const cursor = rows6.at(-1)?.id;
3152
3167
  if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
3153
3168
  startingAfter = cursor;
3154
3169
  }
@@ -3281,7 +3296,7 @@ async function approveApplication(input) {
3281
3296
  try {
3282
3297
  if (!duplicate) {
3283
3298
  const write = await db.transact(
3284
- [{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
3299
+ [{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvedAt: Date.now(), approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
3285
3300
  { mutationId: `approve:${id7}`, guards: [{
3286
3301
  ns: "applications",
3287
3302
  id: id7,
@@ -3564,8 +3579,8 @@ async function issueGiftClaim(db, intent, provider, now) {
3564
3579
  throw new Error("gift issuance conflict");
3565
3580
  }
3566
3581
  }
3567
- function buyerGiftClaims(rows5, now) {
3568
- return rows5.map((row2) => {
3582
+ function buyerGiftClaims(rows6, now) {
3583
+ return rows6.map((row2) => {
3569
3584
  const stored = String(row2.status ?? "pending");
3570
3585
  const status2 = stored === "pending" && Number(row2.codeExpiresAt) <= now ? "expired" : stored;
3571
3586
  return {
@@ -3711,8 +3726,8 @@ async function revokeGiftClaim(db, claim, reason, now) {
3711
3726
  // src/payments-route-support.ts
3712
3727
  async function findPaymentApplication(db, event2) {
3713
3728
  if ("applicationId" in event2 && event2.applicationId) {
3714
- const rows5 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3715
- return rows5.applications?.[0];
3729
+ const rows6 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
3730
+ return rows6.applications?.[0];
3716
3731
  }
3717
3732
  if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
3718
3733
  const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
@@ -3903,6 +3918,306 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3903
3918
  return membershipFail("membership_dispute_retry_required");
3904
3919
  }
3905
3920
 
3921
+ // src/member-system-authority.ts
3922
+ var rows2 = (group) => group.fields.map((id7) => ({ ...group, id: id7 }));
3923
+ var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
3924
+ ...rows2({
3925
+ scope: "network",
3926
+ writer: "network_person",
3927
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3928
+ sensitivity: "pii",
3929
+ memberAccess: "edit",
3930
+ fields: [
3931
+ "person.legalName",
3932
+ "person.preferredName",
3933
+ "person.namePronunciation",
3934
+ "person.pronouns",
3935
+ "person.displayLegalName",
3936
+ "person.primaryEmail",
3937
+ "person.secondaryEmails",
3938
+ "person.phone",
3939
+ "person.linkedinUrl",
3940
+ "person.photo",
3941
+ "person.homeAddress",
3942
+ "person.fulfillmentFit",
3943
+ "person.fulfillmentSize"
3944
+ ]
3945
+ }),
3946
+ ...rows2({
3947
+ scope: "network",
3948
+ writer: "network_person",
3949
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3950
+ sensitivity: "ordinary",
3951
+ memberAccess: "edit",
3952
+ fields: [
3953
+ "person.title",
3954
+ "person.company",
3955
+ "person.companyObservedAt",
3956
+ "person.region",
3957
+ "person.whyText",
3958
+ "person.whyTokens",
3959
+ "person.skillsOffered",
3960
+ "person.openToRequests",
3961
+ "person.directoryPhotoConsent",
3962
+ "person.profilePrompts",
3963
+ "person.introducedByText"
3964
+ ]
3965
+ }),
3966
+ ...rows2({
3967
+ scope: "network",
3968
+ writer: "network_person",
3969
+ projections: ["bnf_admin", "chapter_admin"],
3970
+ sensitivity: "restricted",
3971
+ memberAccess: "none",
3972
+ fields: [
3973
+ "person.linkedinMemberId",
3974
+ "person.identityClaims",
3975
+ "person.aliases",
3976
+ "person.mergeHistory",
3977
+ "person.introducedByPersonId",
3978
+ "person.introducedAt",
3979
+ "person.firstTouch",
3980
+ "person.globalSuppression",
3981
+ "person.deceasedAt",
3982
+ "person.archivedAt"
3983
+ ]
3984
+ }),
3985
+ ...rows2({
3986
+ scope: "network",
3987
+ writer: "network_person",
3988
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3989
+ sensitivity: "sensitive",
3990
+ memberAccess: "edit",
3991
+ fields: ["person.accreditationSelfCertification", "person.accreditationCategory", "person.accreditationExpiresAt"]
3992
+ }),
3993
+ ...rows2({
3994
+ scope: "network",
3995
+ writer: "network_person",
3996
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
3997
+ sensitivity: "restricted",
3998
+ memberAccess: "read",
3999
+ fields: ["person.acknowledgements"]
4000
+ }),
4001
+ ...rows2({
4002
+ scope: "provider",
4003
+ writer: "clerk",
4004
+ projections: ["clerk_identity", "bnf_admin", "chapter_admin"],
4005
+ sensitivity: "restricted",
4006
+ memberAccess: "none",
4007
+ fields: ["identity.clerkUserIds", "identity.clerkEmailVerification"]
4008
+ }),
4009
+ ...rows2({
4010
+ scope: "chapter",
4011
+ writer: "chapter_journey",
4012
+ projections: ["chapter_admin", "chapter_crm", "bnf_admin"],
4013
+ sensitivity: "ordinary",
4014
+ memberAccess: "none",
4015
+ fields: [
4016
+ "journey.chapterId",
4017
+ "journey.stage",
4018
+ "journey.stageEnteredAt",
4019
+ "journey.stageHistory",
4020
+ "journey.ending",
4021
+ "journey.endingReason",
4022
+ "journey.source",
4023
+ "journey.entryChapterId",
4024
+ "journey.route",
4025
+ "journey.flags",
4026
+ "journey.applicationStartedAt",
4027
+ "journey.appliedAt",
4028
+ "journey.paidAt",
4029
+ "journey.callBookedAt",
4030
+ "journey.metAt",
4031
+ "journey.approvedAt",
4032
+ "journey.onboardedAt",
4033
+ "journey.firstLoginAt",
4034
+ "journey.firstDealAt"
4035
+ ]
4036
+ }),
4037
+ ...rows2({
4038
+ scope: "chapter",
4039
+ writer: "chapter_journey",
4040
+ projections: ["chapter_admin", "chapter_crm"],
4041
+ sensitivity: "sensitive",
4042
+ memberAccess: "none",
4043
+ fields: [
4044
+ "journey.relationshipOwnerId",
4045
+ "journey.lastContactAt",
4046
+ "journey.nextAction",
4047
+ "journey.owes",
4048
+ "journey.asks",
4049
+ "journey.preparation",
4050
+ "journey.notes"
4051
+ ]
4052
+ }),
4053
+ ...rows2({
4054
+ scope: "chapter",
4055
+ writer: "chapter_journey",
4056
+ projections: ["chapter_admin", "chapter_crm", "member_self_service"],
4057
+ sensitivity: "ordinary",
4058
+ memberAccess: "edit",
4059
+ fields: ["journey.dealFlowConsent", "journey.directoryConsent", "journey.emailConsent", "journey.smsConsent"]
4060
+ }),
4061
+ ...rows2({
4062
+ scope: "chapter",
4063
+ writer: "chapter_journey",
4064
+ projections: ["chapter_admin", "bnf_admin", "member_self_service"],
4065
+ sensitivity: "ordinary",
4066
+ memberAccess: "read",
4067
+ fields: ["journey.memberNumber"]
4068
+ }),
4069
+ ...rows2({
4070
+ scope: "network",
4071
+ writer: "network_membership",
4072
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4073
+ sensitivity: "ordinary",
4074
+ memberAccess: "read",
4075
+ fields: [
4076
+ "membership.chapterId",
4077
+ "membership.journeyId",
4078
+ "membership.termStartsAt",
4079
+ "membership.termEndsAt",
4080
+ "membership.tier",
4081
+ "membership.status",
4082
+ "membership.joinedAt",
4083
+ "membership.autoRenews",
4084
+ "membership.renewalAt",
4085
+ "membership.cancelsAt",
4086
+ "membership.graceEndsAt",
4087
+ "membership.foundingCohort",
4088
+ "membership.offeringPolicyVersion",
4089
+ "membership.discountPolicyVersion"
4090
+ ]
4091
+ }),
4092
+ ...rows2({
4093
+ scope: "network",
4094
+ writer: "network_commercial_projection",
4095
+ projections: ["bnf_admin", "chapter_admin"],
4096
+ sensitivity: "restricted",
4097
+ memberAccess: "none",
4098
+ fields: [
4099
+ "membership.stripeCustomerId",
4100
+ "membership.stripeSubscriptionId",
4101
+ "membership.stripePriceId",
4102
+ "membership.lastInvoiceId",
4103
+ "membership.paymentOutcome",
4104
+ "membership.refundOutcome"
4105
+ ]
4106
+ }),
4107
+ ...rows2({
4108
+ scope: "network",
4109
+ writer: "network_discount_policy",
4110
+ projections: ["bnf_admin", "chapter_admin", "stripe_billing"],
4111
+ sensitivity: "ordinary",
4112
+ memberAccess: "none",
4113
+ fields: [
4114
+ "discountPolicy.chapterId",
4115
+ "discountPolicy.version",
4116
+ "discountPolicy.eligibleOfferingIds",
4117
+ "discountPolicy.percentOff",
4118
+ "discountPolicy.amountOff",
4119
+ "discountPolicy.activatesOn",
4120
+ "discountPolicy.invoiceCount",
4121
+ "discountPolicy.expiresAt",
4122
+ "discountPolicy.stackingOrder",
4123
+ "discountPolicy.status"
4124
+ ]
4125
+ }),
4126
+ ...rows2({
4127
+ scope: "network",
4128
+ writer: "network_entitlement",
4129
+ projections: ["bnf_admin", "chapter_admin", "member_self_service", "stripe_billing"],
4130
+ sensitivity: "ordinary",
4131
+ memberAccess: "read",
4132
+ fields: [
4133
+ "entitlement.membershipId",
4134
+ "entitlement.kind",
4135
+ "entitlement.priceCents",
4136
+ "entitlement.currency",
4137
+ "entitlement.payerPersonId",
4138
+ "entitlement.purchasedAt",
4139
+ "entitlement.termEndsAt",
4140
+ "entitlement.renewsWithMembership",
4141
+ "entitlement.status"
4142
+ ]
4143
+ }),
4144
+ ...rows2({
4145
+ scope: "network",
4146
+ writer: "network_seat",
4147
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4148
+ sensitivity: "pii",
4149
+ memberAccess: "read",
4150
+ fields: [
4151
+ "seat.membershipId",
4152
+ "seat.entitlementId",
4153
+ "seat.personId",
4154
+ "seat.recipientName",
4155
+ "seat.recipientEmail",
4156
+ "seat.role",
4157
+ "seat.status",
4158
+ "seat.acceptanceExpiresAt",
4159
+ "seat.acceptedAt",
4160
+ "seat.activatedAt",
4161
+ "seat.refundedAt"
4162
+ ]
4163
+ }),
4164
+ ...rows2({
4165
+ scope: "network",
4166
+ writer: "network_referral_ledger",
4167
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4168
+ sensitivity: "ordinary",
4169
+ memberAccess: "read",
4170
+ fields: [
4171
+ "referral.referrerPersonId",
4172
+ "referral.referredPersonId",
4173
+ "referral.qualifiedMembershipId",
4174
+ "referral.creditCents",
4175
+ "referral.status",
4176
+ "referral.appliedInvoiceId",
4177
+ "referral.carryForwardCents",
4178
+ "referral.adjustmentOf"
4179
+ ]
4180
+ }),
4181
+ ...rows2({
4182
+ scope: "network",
4183
+ writer: "network_investment_ledger",
4184
+ projections: ["bnf_admin", "chapter_admin", "member_self_service"],
4185
+ sensitivity: "restricted",
4186
+ memberAccess: "read",
4187
+ fields: [
4188
+ "investment.personId",
4189
+ "investment.chapterId",
4190
+ "investment.season",
4191
+ "investment.purpose",
4192
+ "investment.targetCount",
4193
+ "investment.status",
4194
+ "investment.amountCents",
4195
+ "investment.closedAt",
4196
+ "investment.sourceReference"
4197
+ ]
4198
+ }),
4199
+ ...rows2({
4200
+ scope: "provider",
4201
+ writer: "stripe",
4202
+ projections: ["stripe_billing", "bnf_admin", "chapter_admin"],
4203
+ sensitivity: "restricted",
4204
+ memberAccess: "none",
4205
+ fields: [
4206
+ "commercial.charge",
4207
+ "commercial.invoice",
4208
+ "commercial.refund",
4209
+ "commercial.providerSubscription",
4210
+ "commercial.paymentMethod"
4211
+ ]
4212
+ })
4213
+ ];
4214
+ var MEMBER_SELF_EDITABLE_FIELDS = new Set(
4215
+ MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess === "edit").map((row2) => row2.id)
4216
+ );
4217
+ var MEMBER_SELF_READABLE_FIELDS = new Set(
4218
+ MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
4219
+ );
4220
+
3906
4221
  // src/member-system-contract.ts
3907
4222
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3908
4223
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3914,8 +4229,8 @@ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink)
3914
4229
 
3915
4230
  // src/stripe-webhook-partition.ts
3916
4231
  var firstRow = async (db, ns, q) => {
3917
- const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
3918
- return Array.isArray(rows5) ? rows5[0] : void 0;
4232
+ const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
4233
+ return Array.isArray(rows6) ? rows6[0] : void 0;
3919
4234
  };
3920
4235
  var stripeEventIdentity = (event2) => ({
3921
4236
  ...event2.appId ? { appId: event2.appId } : {},
@@ -4022,8 +4337,8 @@ async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest,
4022
4337
  // src/stripe-event-reconcile.ts
4023
4338
  async function firstRow2(db, ns, q) {
4024
4339
  const res = await db.query({ [ns]: { $: q } });
4025
- const rows5 = res[ns];
4026
- return Array.isArray(rows5) ? rows5[0] : void 0;
4340
+ const rows6 = res[ns];
4341
+ return Array.isArray(rows6) ? rows6[0] : void 0;
4027
4342
  }
4028
4343
  async function reconcileChapterStripeEvent(input, env, ctx) {
4029
4344
  const db = ctx.makeDb(env);
@@ -4340,19 +4655,19 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
4340
4655
  const db = raw2, target = stripeEventTarget(env, ctx);
4341
4656
  const local4 = (row3) => row3.chapterId === target.chapterId && row3.appId === target.appId && row3.environment === target.environment && row3.runtime === target.runtime;
4342
4657
  if (req.method === "GET") {
4343
- const rows5 = (await db.query({ stripeEventReceipts: { $: {
4658
+ const rows6 = (await db.query({ stripeEventReceipts: { $: {
4344
4659
  where: { chapterId: target.chapterId, runtime: target.runtime },
4345
4660
  order: { receivedAt: "desc" },
4346
4661
  limit: 500
4347
4662
  } } })).stripeEventReceipts ?? [];
4348
- const pending = rows5.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
4663
+ const pending = rows6.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
4349
4664
  return json({ rows: pending.map((row3) => ({
4350
4665
  eventId: row3.eventId,
4351
4666
  kind: row3.kind,
4352
4667
  disposition: row3.disposition,
4353
4668
  receivedAt: row3.receivedAt,
4354
4669
  retryReady: row3.retryReady === true
4355
- })), truncated: rows5.length >= 500 });
4670
+ })), truncated: rows6.length >= 500 });
4356
4671
  }
4357
4672
  let eventId;
4358
4673
  try {
@@ -4538,9 +4853,9 @@ function memberApplication(app, meeting, defaultTimezone) {
4538
4853
  var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
4539
4854
  async function memberSessionApplication(db, chapterId, runtime, identity2) {
4540
4855
  const lookup = async (where) => {
4541
- const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4542
- if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
4543
- return rows5.filter((row2) => local(row2, chapterId, runtime));
4856
+ const rows6 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4857
+ if (!Array.isArray(rows6) || rows6.length >= 100) throw new Error("member identity lookup incomplete");
4858
+ return rows6.filter((row2) => local(row2, chapterId, runtime));
4544
4859
  };
4545
4860
  let apps = await lookup({ clerkUserId: identity2.userId });
4546
4861
  let emailFallback = false;
@@ -4917,10 +5232,10 @@ async function namedSeatRecipientClaimId(input, email) {
4917
5232
  }
4918
5233
  async function reserveNamedMembershipSeat(db, input, now) {
4919
5234
  membershipClock(now);
4920
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
5235
+ const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4921
5236
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4922
5237
  const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4923
- if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
5238
+ if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
4924
5239
  const state = await loadMembershipAuthorityState(db, input);
4925
5240
  const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4926
5241
  requireMembershipScope(member, input);
@@ -4937,7 +5252,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4937
5252
  id: id7,
4938
5253
  ...membershipScope(input),
4939
5254
  primaryMembershipId: memberId,
4940
- recipientName: name,
5255
+ recipientName: name2,
4941
5256
  recipientEmailNormalized: email,
4942
5257
  purchaserUserId: input.purchaserUserId,
4943
5258
  recipientClaimId: claimId,
@@ -4984,7 +5299,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4984
5299
  if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4985
5300
  }
4986
5301
  requireMembershipScope(seat, input);
4987
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
5302
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name2 || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4988
5303
  return seat;
4989
5304
  }
4990
5305
 
@@ -5175,9 +5490,9 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5175
5490
  membershipClock(now);
5176
5491
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
5177
5492
  const state = await loadMembershipAuthorityState(db, scope2);
5178
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5493
+ const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5179
5494
  let lifted = 0;
5180
- for (const member of rows5.slice(0, limit)) {
5495
+ for (const member of rows6.slice(0, limit)) {
5181
5496
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
5182
5497
  const offering = membershipOfferingVersion(member.offeringSnapshot);
5183
5498
  if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
@@ -5193,7 +5508,7 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
5193
5508
  ] });
5194
5509
  lifted++;
5195
5510
  }
5196
- return { lifted, more: rows5.length > limit };
5511
+ return { lifted, more: rows6.length > limit };
5197
5512
  }
5198
5513
 
5199
5514
  // src/membership-seat-reprice.ts
@@ -5206,9 +5521,9 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
5206
5521
  const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
5207
5522
  if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
5208
5523
  const target = current.seatPriceCents;
5209
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5210
- const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
5211
- for (const member of rows5.slice(0, limit)) {
5524
+ const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5525
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows6.length > limit };
5526
+ for (const member of rows6.slice(0, limit)) {
5212
5527
  if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
5213
5528
  const id7 = String(member.id), skip = (reason) => {
5214
5529
  receipt.skipped.push({ membershipId: id7, reason });
@@ -5307,8 +5622,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
5307
5622
  }
5308
5623
  }
5309
5624
  if (!one) {
5310
- const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5311
- return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
5625
+ const rows6 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
5626
+ return json({ seats: rows6.slice(0, 200).map(project), truncated: rows6.length > 200 });
5312
5627
  }
5313
5628
  const seat = await membershipLoad(db, "networkSeat", one[1]);
5314
5629
  if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
@@ -5527,8 +5842,8 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5527
5842
  const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
5528
5843
  const found = await db.query({ networkMembership: page, networkSeat: page });
5529
5844
  const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
5530
- const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5531
- const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5845
+ const rows6 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5846
+ const disputes = rows6.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5532
5847
  return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
5533
5848
  }
5534
5849
  const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
@@ -5578,9 +5893,9 @@ function stateOf(item, all, now, window, rivals) {
5578
5893
  var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
5579
5894
  var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
5580
5895
  var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
5581
- function normalize(rows5, collection, parse, invalid2) {
5896
+ function normalize(rows6, collection, parse, invalid2) {
5582
5897
  const out = [];
5583
- for (const row2 of rows5) {
5898
+ for (const row2 of rows6) {
5584
5899
  try {
5585
5900
  out.push({ value: parse(row2.snapshot), row: row2 });
5586
5901
  } catch (error) {
@@ -5591,11 +5906,11 @@ function normalize(rows5, collection, parse, invalid2) {
5591
5906
  }
5592
5907
  async function listMembershipPolicyVersions(db, input) {
5593
5908
  const where = input.chapterId ? { chapterId: input.chapterId } : {};
5594
- const rows5 = await db.query({
5909
+ const rows6 = await db.query({
5595
5910
  membershipOfferingVersion: { $: { where, limit: LIMIT } },
5596
5911
  discountPolicyVersion: { $: { where, limit: LIMIT } }
5597
5912
  });
5598
- const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
5913
+ const offeringRows = rows6.membershipOfferingVersion ?? [], policyRows = rows6.discountPolicyVersion ?? [];
5599
5914
  const invalid2 = [];
5600
5915
  const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
5601
5916
  const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
@@ -5735,22 +6050,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
5735
6050
  const skip = (membershipId, reason) => {
5736
6051
  receipt.skipped.push({ membershipId, reason });
5737
6052
  };
5738
- let rows5;
6053
+ let rows6;
5739
6054
  if (input.membershipIds) {
5740
- rows5 = [];
6055
+ rows6 = [];
5741
6056
  for (const id7 of input.membershipIds) {
5742
6057
  const member = await membershipLoad(db, "networkMembership", id7);
5743
6058
  if (!member) skip(id7, "not_found");
5744
6059
  else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
5745
- else rows5.push(member);
6060
+ else rows6.push(member);
5746
6061
  }
5747
6062
  } else {
5748
6063
  const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5749
6064
  if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
5750
6065
  receipt.more = found.length > limit;
5751
- rows5 = found.slice(0, limit);
6066
+ rows6 = found.slice(0, limit);
5752
6067
  }
5753
- for (const member of rows5) {
6068
+ for (const member of rows6) {
5754
6069
  const id7 = String(member.id);
5755
6070
  if (member.discountPolicyVersionId === policy2.id) {
5756
6071
  receipt.unchanged.push(id7);
@@ -6046,13 +6361,13 @@ var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
6046
6361
  function resolveApplication(a) {
6047
6362
  const required = a?.required ?? DEFAULT_REQUIRED;
6048
6363
  const optional = a?.optional ?? DEFAULT_OPTIONAL;
6049
- for (const [name, arr] of [["required", required], ["optional", optional]]) {
6364
+ for (const [name2, arr] of [["required", required], ["optional", optional]]) {
6050
6365
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
6051
- throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
6366
+ throw new Error(`defineChapter.application.${name2}: must be an array of field-name strings`);
6052
6367
  }
6053
6368
  const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
6054
6369
  if (unsafe) {
6055
- throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
6370
+ throw new Error(`defineChapter.application.${name2}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
6056
6371
  }
6057
6372
  }
6058
6373
  const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
@@ -6104,14 +6419,14 @@ function hasDisclaimerAck(fields) {
6104
6419
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
6105
6420
  function applicantProfile(chapter, fields) {
6106
6421
  const app = chapter.application;
6107
- const allowed2 = (f) => app.profileFields.includes(f);
6422
+ const allowed = (f) => app.profileFields.includes(f);
6108
6423
  const profile = {};
6109
6424
  for (const f of [...app.required, ...app.optional]) {
6110
- if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
6425
+ if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
6111
6426
  const v = fields[f];
6112
6427
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
6113
6428
  }
6114
- if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
6429
+ if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
6115
6430
  return Object.keys(profile).length > 0 ? profile : void 0;
6116
6431
  }
6117
6432
  async function submitApplication(db, chapter, fields, opts) {
@@ -6230,10 +6545,10 @@ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
6230
6545
  applications: { $: { where: { emailNormalized: email }, limit: 10 } },
6231
6546
  $users: { $: { where: { email }, limit: 2 } }
6232
6547
  });
6233
- const rows5 = data.applications ?? [], others = rows5.filter((row2) => row2.id !== applicationId);
6548
+ const rows6 = data.applications ?? [], others = rows6.filter((row2) => row2.id !== applicationId);
6234
6549
  const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
6235
6550
  if (others.length !== closed.length) return conflict();
6236
- const original = rows5.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
6551
+ const original = rows6.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
6237
6552
  const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
6238
6553
  if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
6239
6554
  const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
@@ -6357,15 +6672,15 @@ async function signupControlDigest(document) {
6357
6672
 
6358
6673
  // src/admission-store-read.ts
6359
6674
  async function readAdmissionRows(db, namespace, where, maximum) {
6360
- const rows5 = [];
6675
+ const rows6 = [];
6361
6676
  for (let offset = 0; offset <= maximum; offset += 500) {
6362
6677
  const result = await db.query({ [namespace]: { $: { where, order: { id: "asc" }, limit: 500, offset } } });
6363
6678
  const page = Array.isArray(result[namespace]) ? result[namespace] : [];
6364
- rows5.push(...page);
6365
- if (rows5.length > maximum) throw new Error("admission_assignment_limit");
6679
+ rows6.push(...page);
6680
+ if (rows6.length > maximum) throw new Error("admission_assignment_limit");
6366
6681
  if (page.length < 500) break;
6367
6682
  }
6368
- return rows5;
6683
+ return rows6;
6369
6684
  }
6370
6685
 
6371
6686
  // src/admission-grants.ts
@@ -6448,7 +6763,7 @@ async function createAdmissionGrantEnvelope(document) {
6448
6763
  if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
6449
6764
  return { document: checked.value, digest: await signupControlDigest(checked.value) };
6450
6765
  }
6451
- var rows2 = (value) => Array.isArray(value) ? value : [];
6766
+ var rows3 = (value) => Array.isArray(value) ? value : [];
6452
6767
  function asGrant(row2) {
6453
6768
  if (typeof row2.id !== "string" || typeof row2.emailNormalized !== "string") return null;
6454
6769
  if (row2.sourceScope !== "local" && row2.sourceScope !== "parent") return null;
@@ -6459,7 +6774,7 @@ async function findAdmissionGrant(db, targetId, email, now) {
6459
6774
  const emailNormalized = normalizeAdmissionEmail(email);
6460
6775
  if (!isAdmissionEmail(emailNormalized)) return null;
6461
6776
  const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
6462
- const active = rows2(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6777
+ const active = rows3(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
6463
6778
  grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
6464
6779
  )).sort((a, b) => {
6465
6780
  const scope2 = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
@@ -6473,7 +6788,7 @@ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
6473
6788
  return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
6474
6789
  }
6475
6790
  const found = await db.query({ admissionGrantRevisions: {} });
6476
- return rows2(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
6791
+ return rows3(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
6477
6792
  }
6478
6793
  async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
6479
6794
  if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
@@ -6482,7 +6797,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
6482
6797
  order: { revision: "desc" },
6483
6798
  limit: 1
6484
6799
  } } });
6485
- const row2 = rows2(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
6800
+ const row2 = rows3(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
6486
6801
  return row2 ? row2 : null;
6487
6802
  }
6488
6803
  async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
@@ -6561,8 +6876,8 @@ var APPLICATION_LABELS = {
6561
6876
  state: "State"
6562
6877
  };
6563
6878
  function humanize(id7) {
6564
- const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6565
- return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
6879
+ const text11 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6880
+ return text11 ? `${text11[0].toUpperCase()}${text11.slice(1)}` : id7;
6566
6881
  }
6567
6882
  function applicationDefs(chapter) {
6568
6883
  try {
@@ -6655,8 +6970,8 @@ function validateGiftOfferCatalog(input, tierIds) {
6655
6970
  errors.push(`${path} must be an object`);
6656
6971
  continue;
6657
6972
  }
6658
- const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6659
- for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6973
+ const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6974
+ for (const key3 of Object.keys(item)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
6660
6975
  const id7 = text5(item.id, `${path}.id`, errors, 80);
6661
6976
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6662
6977
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
@@ -6720,27 +7035,27 @@ async function hasSignupControl(db, targetId, environment, active) {
6720
7035
  if (db.aggregate) {
6721
7036
  return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
6722
7037
  }
6723
- const rows5 = (await db.query({ signupControls: {} })).signupControls;
6724
- return Array.isArray(rows5) && rows5.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
7038
+ const rows6 = (await db.query({ signupControls: {} })).signupControls;
7039
+ return Array.isArray(rows6) && rows6.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
6725
7040
  }
6726
7041
  async function loadLatestSignupControl(db, targetId, environment) {
6727
7042
  if (!await hasSignupControl(db, targetId, environment)) return null;
6728
- const rows5 = (await db.query({ signupControls: { $: {
7043
+ const rows6 = (await db.query({ signupControls: { $: {
6729
7044
  where: { targetId, environment },
6730
7045
  order: { revision: "desc" },
6731
7046
  limit: 1
6732
7047
  } } })).signupControls;
6733
- const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
7048
+ const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6734
7049
  return asStored(latest);
6735
7050
  }
6736
7051
  async function loadActiveSignupControl(db, targetId, environment) {
6737
7052
  if (!await hasSignupControl(db, targetId, environment, true)) return null;
6738
- const rows5 = (await db.query({ signupControls: { $: {
7053
+ const rows6 = (await db.query({ signupControls: { $: {
6739
7054
  where: { targetId, environment, active: true },
6740
7055
  order: { revision: "desc" },
6741
7056
  limit: 1
6742
7057
  } } })).signupControls;
6743
- const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
7058
+ const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
6744
7059
  return asStored(active);
6745
7060
  }
6746
7061
  function controlledApplication(base, document) {
@@ -7113,17 +7428,17 @@ async function readBoundedRequestText(req, maxBytes) {
7113
7428
  if (!reader) return "";
7114
7429
  const decoder = new TextDecoder();
7115
7430
  let bytes = 0;
7116
- let text10 = "";
7431
+ let text11 = "";
7117
7432
  for (; ; ) {
7118
7433
  const { done, value } = await reader.read();
7119
7434
  if (req.signal.aborted) return invalid2();
7120
7435
  if (done) {
7121
7436
  complete = true;
7122
- return text10 + decoder.decode();
7437
+ return text11 + decoder.decode();
7123
7438
  }
7124
7439
  bytes += value.byteLength;
7125
7440
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
7126
- text10 += decoder.decode(value, { stream: true });
7441
+ text11 += decoder.decode(value, { stream: true });
7127
7442
  }
7128
7443
  } catch {
7129
7444
  return invalid2();
@@ -7227,9 +7542,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
7227
7542
  let selection;
7228
7543
  if (request?.body) {
7229
7544
  try {
7230
- const text10 = await readBoundedRequestText(request, 2048);
7231
- if (text10 instanceof Response) return text10;
7232
- const body = JSON.parse(text10);
7545
+ const text11 = await readBoundedRequestText(request, 2048);
7546
+ if (text11 instanceof Response) return text11;
7547
+ const body = JSON.parse(text11);
7233
7548
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
7234
7549
  selection = body.namedSeat;
7235
7550
  } catch {
@@ -7382,9 +7697,9 @@ async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
7382
7697
  if (!app || app.groupId !== chapterId || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(runtime)) {
7383
7698
  throw new Error("invitation purchaser unavailable");
7384
7699
  }
7385
- const name = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7386
- if (!name) throw new Error("invitation purchaser name unavailable");
7387
- return name.slice(0, 160);
7700
+ const name2 = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
7701
+ if (!name2) throw new Error("invitation purchaser name unavailable");
7702
+ return name2.slice(0, 160);
7388
7703
  }
7389
7704
  async function namedSeatApplicationContext(req, seatId, env, ctx) {
7390
7705
  const seats = ctx.membershipAuthority?.namedSeats;
@@ -7413,10 +7728,10 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
7413
7728
  }
7414
7729
  async function existingNamedSeatApplication(context, env, ctx) {
7415
7730
  const { db, userId, email, invitation } = context;
7416
- const rows5 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7417
- const apps = rows5.filter((row2) => !closedApplication(row2));
7731
+ const rows6 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
7732
+ const apps = rows6.filter((row2) => !closedApplication(row2));
7418
7733
  if (!apps.length) {
7419
- const closed = rows5.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
7734
+ const closed = rows6.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
7420
7735
  return { predecessor: closed ? { id: String(closed.id) } : null };
7421
7736
  }
7422
7737
  const app = apps[0];
@@ -7781,6 +8096,111 @@ async function notifySubmitConfirmed(db, env, notice) {
7781
8096
  }
7782
8097
  }
7783
8098
 
8099
+ // src/member-entitlement.ts
8100
+ function entitledTierId(app, now = Date.now()) {
8101
+ const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
8102
+ const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
8103
+ if (granted && (endsAt === null || endsAt > now)) return granted;
8104
+ return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
8105
+ }
8106
+
8107
+ // src/portal-member.ts
8108
+ var text6 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8109
+ async function activeHubGrant(db, identity2) {
8110
+ const byUser = (await db.query({ hubMemberGrants: { $: { where: { userId: identity2.userId }, limit: 5 } } })).hubMemberGrants ?? [];
8111
+ const byEmail = identity2.email ? (await db.query({ hubMemberGrants: { $: { where: { emailNormalized: normalizeChapterEmail(identity2.email) }, limit: 1 } } })).hubMemberGrants ?? [] : [];
8112
+ return [...byUser, ...byEmail].find((row2) => row2.status === "active") ?? null;
8113
+ }
8114
+ async function resolvePortalMember(db, env, ctx, identity2) {
8115
+ try {
8116
+ if (ctx.chapter.mode === "hub") {
8117
+ const grant = await activeHubGrant(db, identity2);
8118
+ if (!grant || !identity2.email) return { status: "not-member" };
8119
+ return { status: "member", member: {
8120
+ memberId: String(grant.id),
8121
+ source: "hub-grant",
8122
+ email: identity2.email,
8123
+ ...text6(grant.firstName) ? { firstName: text6(grant.firstName) } : {},
8124
+ ...text6(grant.lastName) ? { lastName: text6(grant.lastName) } : {},
8125
+ tierId: null,
8126
+ memberSince: Number(grant.grantedAt) || null,
8127
+ row: grant
8128
+ } };
8129
+ }
8130
+ const session = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, identity2);
8131
+ if (!session) return { status: "not-member" };
8132
+ const app = session.source;
8133
+ if (!await chapterMembershipAccess(db, env, ctx, app)) return { status: "not-member", row: app };
8134
+ return { status: "member", member: {
8135
+ memberId: String(app.id),
8136
+ source: "application",
8137
+ email: String(app.email ?? identity2.email ?? ""),
8138
+ ...text6(app.firstName) ? { firstName: text6(app.firstName) } : {},
8139
+ ...text6(app.lastName) ? { lastName: text6(app.lastName) } : {},
8140
+ tierId: entitledTierId(app),
8141
+ memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
8142
+ row: app
8143
+ } };
8144
+ } catch {
8145
+ return { status: "unavailable" };
8146
+ }
8147
+ }
8148
+ function initialsOf(name2) {
8149
+ const words2 = name2.trim().split(/\s+/).filter(Boolean);
8150
+ const letters = words2.length > 1 ? [words2[0], words2[words2.length - 1]] : words2;
8151
+ return letters.map((word) => [...word][0].toUpperCase()).join("") || "?";
8152
+ }
8153
+ async function portalIdentity(db, ctx, member) {
8154
+ const profile = (await db.query({ memberProfiles: { $: { where: { id: member.memberId }, limit: 1 } } })).memberProfiles?.[0];
8155
+ let tier = null;
8156
+ if (member.tierId) {
8157
+ const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
8158
+ const match = group ? (await loadTiers(db, group)).find((candidate) => candidate.id === member.tierId) : void 0;
8159
+ tier = { id: member.tierId, name: match?.name ?? member.tierId };
8160
+ }
8161
+ const legal = [member.firstName, member.lastName].filter(Boolean).join(" ");
8162
+ const displayName = text6(profile?.preferredName) ?? (legal || member.email);
8163
+ const photoPath = text6(profile?.photoPath);
8164
+ const photoUrl = photoPath && db.storage && ctx.chapter.memberPortal?.photos ? await db.storage.sign(photoPath, 3600).catch(() => null) : null;
8165
+ return {
8166
+ memberId: member.memberId,
8167
+ tier,
8168
+ memberSince: member.memberSince,
8169
+ displayName,
8170
+ firstName: text6(profile?.preferredName)?.split(/\s+/)[0] ?? member.firstName ?? null,
8171
+ initials: initialsOf(displayName),
8172
+ location: text6(profile?.location) ?? null,
8173
+ photoUrl
8174
+ };
8175
+ }
8176
+
8177
+ // src/portal-me.ts
8178
+ var servesMembers = (ctx) => ctx.chapter.mode === "chapter" || Boolean(ctx.chapter.memberPortal || ctx.chapter.memberContent);
8179
+ async function hubMe(db, env, ctx, identity2, base) {
8180
+ if (!servesMembers(ctx)) return base;
8181
+ const result = await resolvePortalMember(db, env, ctx, identity2);
8182
+ if (result.status === "unavailable") return { ...base, memberAccess: null, portal: null };
8183
+ if (result.status !== "member") return { ...base, memberAccess: false, portal: null };
8184
+ return { ...base, memberAccess: true, portal: await identityOrNull(db, ctx, result.member) };
8185
+ }
8186
+ async function chapterPortalIdentity(db, ctx, memberAccess, app) {
8187
+ if (!memberAccess || !app || !ctx.chapter.memberPortal) return null;
8188
+ const text11 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8189
+ return identityOrNull(db, ctx, {
8190
+ memberId: String(app.id),
8191
+ source: "application",
8192
+ email: String(app.email ?? ""),
8193
+ ...text11(app.firstName) ? { firstName: text11(app.firstName) } : {},
8194
+ ...text11(app.lastName) ? { lastName: text11(app.lastName) } : {},
8195
+ tierId: entitledTierId(app),
8196
+ memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
8197
+ row: app
8198
+ });
8199
+ }
8200
+ async function identityOrNull(db, ctx, member) {
8201
+ return portalIdentity(db, ctx, member).catch(() => null);
8202
+ }
8203
+
7784
8204
  // src/worker-routes-health.ts
7785
8205
  var handleConfig = async (_req, url, env, ctx) => {
7786
8206
  if (url.pathname !== "/api/config") return null;
@@ -7795,13 +8215,13 @@ var handleHealth = async (_req, url, env, ctx) => {
7795
8215
  if (url.pathname !== "/api/health") return null;
7796
8216
  const response = json({ ok: true });
7797
8217
  response.headers.set("cache-control", "no-store");
7798
- for (const [name, value] of Object.entries({
8218
+ for (const [name2, value] of Object.entries({
7799
8219
  "x-odla-app-id": env.ODLA_APP_ID,
7800
8220
  "x-odla-chapter-id": ctx.chapter.id,
7801
8221
  "x-odla-data-environment": env.ODLA_ENV,
7802
8222
  "x-odla-runtime": env.ODLA_RUNTIME || "default"
7803
8223
  })) {
7804
- if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name, value);
8224
+ if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name2, value);
7805
8225
  }
7806
8226
  return response;
7807
8227
  };
@@ -7815,8 +8235,10 @@ var handleMe = async (req, url, env, ctx) => {
7815
8235
  const db = ctx.makeDb(env);
7816
8236
  const role = await ctx.roleFor(db, u);
7817
8237
  const superAdmin = await ctx.isSuperAdminEmail(db, u.email, u.userId);
7818
- const base = { authorized: superAdmin || isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
7819
- if (ctx.chapter.mode !== "chapter") return json(base);
8238
+ const admin = superAdmin || isAdminRole(role, ctx.auth);
8239
+ const secondFactorOk = !admin || adminSecondFactorAllows(ctx.auth.adminSecondFactor ?? "off", u.payload, u.email);
8240
+ const base = { authorized: admin && secondFactorOk, role, superAdmin, email: u.email ?? null, ...admin && !secondFactorOk ? { secondFactorRequired: true } : {} };
8241
+ if (ctx.chapter.mode !== "chapter") return json(await hubMe(db, env, ctx, u, base));
7820
8242
  let member;
7821
8243
  try {
7822
8244
  member = await memberSessionApplication(
@@ -7852,7 +8274,8 @@ var handleMe = async (req, url, env, ctx) => {
7852
8274
  } catch {
7853
8275
  }
7854
8276
  }
7855
- return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
8277
+ const portal = await chapterPortalIdentity(db, ctx, memberAccess, member?.source);
8278
+ return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, portal, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
7856
8279
  };
7857
8280
  var handleCrm = async (req, url, env, ctx) => {
7858
8281
  const crmBase = ctx.crmBase;
@@ -8097,9 +8520,9 @@ var MAX_BENEFITS = 24;
8097
8520
  var MAX_MARKDOWN = 65536;
8098
8521
  var MAX_LABEL = 160;
8099
8522
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8100
- function rejectUnknownKeys(value, allowed2, path, errors) {
8523
+ function rejectUnknownKeys(value, allowed, path, errors) {
8101
8524
  for (const key3 of Object.keys(value)) {
8102
- if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8525
+ if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8103
8526
  }
8104
8527
  }
8105
8528
  function boundedString(value, path, errors, max, required = false) {
@@ -8111,10 +8534,10 @@ function boundedString(value, path, errors, max, required = false) {
8111
8534
  errors.push(`${path} must be a string`);
8112
8535
  return void 0;
8113
8536
  }
8114
- const text10 = value.trim();
8115
- if (required && !text10) errors.push(`${path} is required`);
8116
- if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8117
- return text10;
8537
+ const text11 = value.trim();
8538
+ if (required && !text11) errors.push(`${path} is required`);
8539
+ if (text11.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8540
+ return text11;
8118
8541
  }
8119
8542
  function validateMembershipPageDocument(input, options = {}) {
8120
8543
  const errors = [];
@@ -8163,8 +8586,8 @@ function validateMembershipPageDocument(input, options = {}) {
8163
8586
  else {
8164
8587
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
8165
8588
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
8166
- const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8167
- return text10 ? [text10] : [];
8589
+ const text11 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8590
+ return text11 ? [text11] : [];
8168
8591
  });
8169
8592
  }
8170
8593
  }
@@ -8255,10 +8678,10 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
8255
8678
  ]);
8256
8679
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
8257
8680
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8258
- function unknownKeys(value, allowed2, path, errors) {
8259
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8681
+ function unknownKeys(value, allowed, path, errors) {
8682
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8260
8683
  }
8261
- function text6(value, path, errors, max = 65536) {
8684
+ function text7(value, path, errors, max = 65536) {
8262
8685
  if (typeof value !== "string" || !value.trim()) {
8263
8686
  errors.push(`${path} is required`);
8264
8687
  return "";
@@ -8281,11 +8704,11 @@ function tiersOf(value, errors) {
8281
8704
  return [];
8282
8705
  }
8283
8706
  unknownKeys(item, TIER_KEYS2, path, errors);
8284
- const id7 = text6(item.id, `${path}.id`, errors, 80);
8707
+ const id7 = text7(item.id, `${path}.id`, errors, 80);
8285
8708
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8286
8709
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
8287
8710
  seen.add(id7);
8288
- const name = text6(item.name, `${path}.name`, errors, 160);
8711
+ const name2 = text7(item.name, `${path}.name`, errors, 160);
8289
8712
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
8290
8713
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
8291
8714
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -8303,10 +8726,10 @@ function tiersOf(value, errors) {
8303
8726
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
8304
8727
  errors.push(`${path}.interval must be year or month`);
8305
8728
  }
8306
- const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
8729
+ const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
8307
8730
  return [{
8308
8731
  id: id7,
8309
- name,
8732
+ name: name2,
8310
8733
  available: item.available === true,
8311
8734
  sortOrder: Number(item.sortOrder) || 0,
8312
8735
  priceCents: Number(item.priceCents) || 0,
@@ -8349,7 +8772,7 @@ function policyOf(value, errors, tiers) {
8349
8772
  errors.push("document.policy must be an object");
8350
8773
  return void 0;
8351
8774
  }
8352
- const allowed2 = /* @__PURE__ */ new Set([
8775
+ const allowed = /* @__PURE__ */ new Set([
8353
8776
  "disclaimerText",
8354
8777
  "refundPolicyText",
8355
8778
  "merchantDisclosureText",
@@ -8357,11 +8780,11 @@ function policyOf(value, errors, tiers) {
8357
8780
  "commitmentText",
8358
8781
  "normsText"
8359
8782
  ]);
8360
- unknownKeys(input, /* @__PURE__ */ new Set([...allowed2, "tierPolicies"]), "document.policy", errors);
8783
+ unknownKeys(input, /* @__PURE__ */ new Set([...allowed, "tierPolicies"]), "document.policy", errors);
8361
8784
  const result = {};
8362
- for (const key3 of allowed2) {
8785
+ for (const key3 of allowed) {
8363
8786
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
8364
- result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
8787
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
8365
8788
  }
8366
8789
  const tierPolicies = {};
8367
8790
  if (input.tierPolicies !== void 0) {
@@ -8376,8 +8799,8 @@ function policyOf(value, errors, tiers) {
8376
8799
  }
8377
8800
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
8378
8801
  tierPolicies[id7] = {
8379
- refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8380
- ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8802
+ refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8803
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8381
8804
  };
8382
8805
  }
8383
8806
  }
@@ -8417,9 +8840,9 @@ function validateSignupControlDocument(input) {
8417
8840
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
8418
8841
  const source = recordOf(root.source);
8419
8842
  const target = recordOf(root.target);
8420
- const appId = text6(source?.appId, "document.source.appId", errors, 63);
8421
- const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
8422
- const environment = text6(target?.environment, "document.target.environment", errors, 63);
8843
+ const appId = text7(source?.appId, "document.source.appId", errors, 63);
8844
+ const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
8845
+ const environment = text7(target?.environment, "document.target.environment", errors, 63);
8423
8846
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
8424
8847
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
8425
8848
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -8560,14 +8983,45 @@ async function memberContentDigest(document) {
8560
8983
  return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
8561
8984
  }
8562
8985
 
8986
+ // src/member-content-kinds.ts
8987
+ var MEMBER_PAGE_FEATURES = ["course", "share"];
8988
+ var MEMBER_PAGE_ICONS = [
8989
+ "home",
8990
+ "course",
8991
+ "library",
8992
+ "deals",
8993
+ "training",
8994
+ "events",
8995
+ "members",
8996
+ "share",
8997
+ "profile",
8998
+ "document",
8999
+ "calendar",
9000
+ "chat",
9001
+ "star"
9002
+ ];
9003
+ var MEMBER_PAGE_TAGS = ["new"];
9004
+ function composeMemberSections(parent, chapter, pages) {
9005
+ const parentDeclared = new Map((parent?.sections ?? []).map((section) => [section.id, section]));
9006
+ const out = /* @__PURE__ */ new Map();
9007
+ for (const section of chapter?.sections ?? []) {
9008
+ const label = section.label ?? parentDeclared.get(section.id)?.label;
9009
+ out.set(section.id, { id: section.id, ...label ? { label } : {} });
9010
+ }
9011
+ for (const section of parent?.sections ?? []) if (!out.has(section.id)) out.set(section.id, section);
9012
+ const named = [...new Set(pages.map((page) => page.navigation.section))];
9013
+ for (const id7 of named.filter((id8) => !out.has(id8)).sort((a, b) => a.localeCompare(b))) out.set(id7, { id: id7, label: id7 });
9014
+ return [...out.values()].filter((section) => named.includes(section.id));
9015
+ }
9016
+
8563
9017
  // src/member-content-validation-helpers.ts
8564
9018
  var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
8565
9019
  var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8566
- var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
9020
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides", "sections"]);
8567
9021
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
8568
9022
  var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
8569
- var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
8570
- var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
9023
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell", "kind", "placeholder", "feature"]);
9024
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId", "tag", "icon"]);
8571
9025
  var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
8572
9026
  var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
8573
9027
  var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
@@ -8576,8 +9030,8 @@ var MAX_PAGES = 100;
8576
9030
  var MAX_MARKDOWN2 = 65536;
8577
9031
  var MAX_TEXT = 240;
8578
9032
  var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8579
- function unknownKeys2(value, allowed2, path, errors) {
8580
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
9033
+ function unknownKeys2(value, allowed, path, errors) {
9034
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
8581
9035
  }
8582
9036
  function textValue(value, path, errors, max = MAX_TEXT, required = false) {
8583
9037
  if (value === void 0) {
@@ -8617,13 +9071,140 @@ function offerValue(value, path, errors, tiers, audience) {
8617
9071
  };
8618
9072
  }
8619
9073
 
9074
+ // src/member-content-validation-v2.ts
9075
+ var SECTION_KEYS = /* @__PURE__ */ new Set(["id", "label"]);
9076
+ var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set(["availableYear", "badge", "actionLabel"]);
9077
+ var FEATURE_KEYS = /* @__PURE__ */ new Set(["name", "ref"]);
9078
+ var MAX_SECTIONS = 24;
9079
+ var V2_ROOT_KEYS = ["sections"];
9080
+ var V2_PAGE_KEYS = ["kind", "placeholder", "feature"];
9081
+ var V2_NAV_KEYS = ["tag", "icon"];
9082
+ function rejectV2Fields(raw2, keys3, path, errors) {
9083
+ for (const key3 of keys3) if (raw2[key3] !== void 0) errors.push(`${path}.${key3} requires schemaVersion 2`);
9084
+ }
9085
+ function sectionsValue(value, errors) {
9086
+ if (value === void 0) return void 0;
9087
+ if (!Array.isArray(value)) {
9088
+ errors.push("document.sections must be an array");
9089
+ return void 0;
9090
+ }
9091
+ if (value.length > MAX_SECTIONS) errors.push(`document.sections supports at most ${MAX_SECTIONS} entries`);
9092
+ const seen = /* @__PURE__ */ new Set();
9093
+ const sections = [];
9094
+ for (const [index2, item] of value.slice(0, MAX_SECTIONS).entries()) {
9095
+ const path = `document.sections[${index2}]`;
9096
+ const raw2 = record2(item);
9097
+ if (!raw2) {
9098
+ errors.push(`${path} must be an object`);
9099
+ continue;
9100
+ }
9101
+ unknownKeys2(raw2, SECTION_KEYS, path, errors);
9102
+ const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
9103
+ if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9104
+ if (seen.has(id7)) errors.push(`${path}.id duplicates section "${id7}"`);
9105
+ seen.add(id7);
9106
+ const label = textValue(raw2.label, `${path}.label`, errors, 120);
9107
+ sections.push({ id: id7, ...label ? { label } : {} });
9108
+ }
9109
+ return sections;
9110
+ }
9111
+ var oneOf = (values, value) => typeof value === "string" && values.includes(value);
9112
+ function navigationV2(raw2, path, errors) {
9113
+ const out = {};
9114
+ if (raw2.tag !== void 0) {
9115
+ if (oneOf(MEMBER_PAGE_TAGS, raw2.tag)) out.tag = raw2.tag;
9116
+ else errors.push(`${path}.tag must be one of ${MEMBER_PAGE_TAGS.join(", ")}`);
9117
+ }
9118
+ if (raw2.icon !== void 0) {
9119
+ if (oneOf(MEMBER_PAGE_ICONS, raw2.icon)) out.icon = raw2.icon;
9120
+ else errors.push(`${path}.icon must be one of ${MEMBER_PAGE_ICONS.join(", ")}`);
9121
+ }
9122
+ return out;
9123
+ }
9124
+ function placeholderValue(value, path, errors) {
9125
+ const raw2 = record2(value);
9126
+ if (!raw2) {
9127
+ errors.push(`${path} must be an object`);
9128
+ return {};
9129
+ }
9130
+ unknownKeys2(raw2, PLACEHOLDER_KEYS, path, errors);
9131
+ const year = raw2.availableYear;
9132
+ if (year !== void 0 && (!Number.isInteger(year) || Number(year) < 2e3 || Number(year) > 2100)) {
9133
+ errors.push(`${path}.availableYear must be a year from 2000 to 2100`);
9134
+ }
9135
+ const badge = textValue(raw2.badge, `${path}.badge`, errors, 60);
9136
+ const actionLabel = textValue(raw2.actionLabel, `${path}.actionLabel`, errors, 80);
9137
+ return {
9138
+ ...Number.isInteger(year) ? { availableYear: Number(year) } : {},
9139
+ ...badge ? { badge } : {},
9140
+ ...actionLabel ? { actionLabel } : {}
9141
+ };
9142
+ }
9143
+ function featureValue(value, path, errors) {
9144
+ const raw2 = record2(value);
9145
+ if (!raw2) {
9146
+ errors.push(`${path} must be an object`);
9147
+ return void 0;
9148
+ }
9149
+ unknownKeys2(raw2, FEATURE_KEYS, path, errors);
9150
+ if (!oneOf(MEMBER_PAGE_FEATURES, raw2.name)) {
9151
+ errors.push(`${path}.name must be one of ${MEMBER_PAGE_FEATURES.join(", ")}`);
9152
+ return void 0;
9153
+ }
9154
+ const ref = textValue(raw2.ref, `${path}.ref`, errors, 80);
9155
+ if (ref && !SLUG2.test(ref)) errors.push(`${path}.ref must be a lowercase slug`);
9156
+ return { name: raw2.name, ...ref ? { ref } : {} };
9157
+ }
9158
+ function pageKindValue(raw2, path, errors) {
9159
+ const kind = raw2.kind ?? "markdown";
9160
+ if (kind !== "markdown" && kind !== "placeholder" && kind !== "feature") {
9161
+ errors.push(`${path}.kind must be markdown, placeholder or feature`);
9162
+ return {};
9163
+ }
9164
+ if (kind !== "placeholder" && raw2.placeholder !== void 0) errors.push(`${path}.placeholder requires kind placeholder`);
9165
+ if (kind !== "feature" && raw2.feature !== void 0) errors.push(`${path}.feature requires kind feature`);
9166
+ if (kind === "markdown" && raw2.bodyMarkdown === void 0) errors.push(`${path}.bodyMarkdown is required`);
9167
+ if (kind === "feature" && raw2.bodyMarkdown !== void 0) errors.push(`${path}.bodyMarkdown is not supported on a feature page`);
9168
+ const out = raw2.kind === void 0 ? {} : { kind };
9169
+ if (kind === "placeholder") out.placeholder = placeholderValue(raw2.placeholder ?? {}, `${path}.placeholder`, errors);
9170
+ if (kind === "feature") {
9171
+ if (raw2.feature === void 0) errors.push(`${path}.feature is required for kind feature`);
9172
+ else {
9173
+ const feature = featureValue(raw2.feature, `${path}.feature`, errors);
9174
+ if (feature) out.feature = feature;
9175
+ }
9176
+ }
9177
+ return out;
9178
+ }
9179
+ function duplicateFeatures(pages, errors) {
9180
+ const seen = /* @__PURE__ */ new Set();
9181
+ for (const [index2, page] of pages.entries()) {
9182
+ if (!page.feature) continue;
9183
+ const key3 = `${page.feature.name}:${page.feature.ref ?? ""}`;
9184
+ if (seen.has(key3)) errors.push(`document.pages[${index2}].feature duplicates ${page.feature.name}${page.feature.ref ? ` "${page.feature.ref}"` : ""}`);
9185
+ seen.add(key3);
9186
+ }
9187
+ }
9188
+ function sectionReferences(pages, sections, parentPages, errors) {
9189
+ if (!sections) return;
9190
+ const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), ...parentPages.map((page) => page.navigation.section)]);
9191
+ for (const [index2, page] of pages.entries()) {
9192
+ if (!known.has(page.navigation.section)) {
9193
+ errors.push(`document.pages[${index2}].navigation.section references undeclared section "${page.navigation.section}"`);
9194
+ }
9195
+ }
9196
+ }
9197
+
8620
9198
  // src/member-content-validation.ts
8621
9199
  function validateMemberContentDocument(input, options = {}) {
8622
9200
  const errors = [];
8623
9201
  const root = record2(input);
8624
9202
  if (!root) return { ok: false, errors: ["document must be an object"] };
8625
9203
  unknownKeys2(root, ROOT_KEYS3, "document", errors);
8626
- if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
9204
+ if (root.schemaVersion !== 1 && root.schemaVersion !== 2) errors.push("document.schemaVersion must be 1 or 2");
9205
+ const v2 = root.schemaVersion === 2;
9206
+ if (!v2) rejectV2Fields(root, V2_ROOT_KEYS, "document", errors);
9207
+ const sections = v2 ? sectionsValue(root.sections, errors) : void 0;
8627
9208
  if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
8628
9209
  if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
8629
9210
  const sourceRaw = record2(root.source);
@@ -8668,6 +9249,8 @@ function validateMemberContentDocument(input, options = {}) {
8668
9249
  continue;
8669
9250
  }
8670
9251
  unknownKeys2(raw2, PAGE_KEYS, path, errors);
9252
+ if (!v2) rejectV2Fields(raw2, V2_PAGE_KEYS, path, errors);
9253
+ const kindFields = v2 ? pageKindValue(raw2, path, errors) : {};
8671
9254
  const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
8672
9255
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
8673
9256
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
@@ -8682,13 +9265,19 @@ function validateMemberContentDocument(input, options = {}) {
8682
9265
  if (!navRaw) errors.push(`${path}.navigation must be an object`);
8683
9266
  else {
8684
9267
  unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
9268
+ if (!v2) rejectV2Fields(navRaw, V2_NAV_KEYS, `${path}.navigation`, errors);
8685
9269
  const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
8686
9270
  const order2 = navRaw.order;
8687
9271
  if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
8688
9272
  errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
8689
9273
  }
8690
9274
  const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
8691
- navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
9275
+ navigation = {
9276
+ section,
9277
+ order: Number.isInteger(order2) ? Number(order2) : index2,
9278
+ ...parentId ? { parentId } : {},
9279
+ ...v2 ? navigationV2(navRaw, `${path}.navigation`, errors) : {}
9280
+ };
8692
9281
  }
8693
9282
  const audienceRaw = record2(raw2.audience);
8694
9283
  let audience = { kind: "all-members" };
@@ -8714,7 +9303,7 @@ function validateMemberContentDocument(input, options = {}) {
8714
9303
  }
8715
9304
  const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
8716
9305
  const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
8717
- const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
9306
+ const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, !v2);
8718
9307
  let upsell;
8719
9308
  if (raw2.upsell !== void 0) {
8720
9309
  const upsellRaw = record2(raw2.upsell);
@@ -8731,7 +9320,22 @@ function validateMemberContentDocument(input, options = {}) {
8731
9320
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
8732
9321
  }
8733
9322
  }
8734
- pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
9323
+ pages.push({
9324
+ id: id7,
9325
+ slug,
9326
+ title,
9327
+ ...summary ? { summary } : {},
9328
+ navigation,
9329
+ audience,
9330
+ ...teaserMarkdown ? { teaserMarkdown } : {},
9331
+ ...bodyMarkdown !== void 0 ? { bodyMarkdown } : {},
9332
+ ...upsell ? { upsell } : {},
9333
+ ...kindFields
9334
+ });
9335
+ }
9336
+ if (v2) {
9337
+ duplicateFeatures(pages, errors);
9338
+ sectionReferences(pages, sections, options.parentPages ?? [], errors);
8735
9339
  }
8736
9340
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
8737
9341
  for (const [index2, page] of pages.entries()) {
@@ -8778,21 +9382,21 @@ function validateMemberContentDocument(input, options = {}) {
8778
9382
  }
8779
9383
  if (errors.length) return { ok: false, errors };
8780
9384
  return { ok: true, value: {
8781
- schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
9385
+ schemaVersion: v2 ? 2 : 1,
8782
9386
  revision: Number(root.revision),
8783
9387
  generatedAt: Number(root.generatedAt),
8784
9388
  source,
8785
9389
  ...target ? { target } : {},
8786
9390
  pages,
8787
- ...upsellOverrides ? { upsellOverrides } : {}
9391
+ ...upsellOverrides ? { upsellOverrides } : {},
9392
+ ...sections ? { sections } : {}
8788
9393
  } };
8789
9394
  }
8790
9395
 
8791
9396
  // src/member-content.ts
8792
- var MEMBER_CONTENT_SCHEMA_VERSION = 1;
8793
9397
  var MEMBER_CONTENT_PATH = "/api/member-content";
8794
9398
  var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
8795
- var pageOrder = (a, b) => a.navigation.section.localeCompare(b.navigation.section) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
9399
+ var pageOrder = (rank) => (a, b) => (rank.get(a.navigation.section) ?? 0) - (rank.get(b.navigation.section) ?? 0) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
8796
9400
  function composeMemberContent(parent, chapter) {
8797
9401
  const pages = [];
8798
9402
  const parentPages = /* @__PURE__ */ new Map();
@@ -8811,20 +9415,24 @@ function composeMemberContent(parent, chapter) {
8811
9415
  }
8812
9416
  pages.push({ ...page, source: chapter.source });
8813
9417
  }
9418
+ const sections = composeMemberSections(parent, chapter, pages);
9419
+ const rank = new Map(sections.map((section, index2) => [section.id, index2]));
8814
9420
  return {
8815
- pages: pages.sort(pageOrder),
9421
+ pages: pages.sort(pageOrder(rank)),
9422
+ sections,
8816
9423
  parentRevision: parent?.revision ?? null,
8817
9424
  chapterRevision: chapter?.revision ?? null
8818
9425
  };
8819
9426
  }
8820
- var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
9427
+ var memberPageAllowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
8821
9428
  function projectMemberContent(content, viewer, tiers) {
8822
9429
  const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
8823
9430
  return {
8824
9431
  viewer: { ...viewer },
8825
9432
  revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
9433
+ sections: content.sections ?? composeMemberSections(null, null, content.pages),
8826
9434
  pages: content.pages.map((page) => {
8827
- const canRead = allowed(page, viewer);
9435
+ const canRead = memberPageAllowed(page, viewer);
8828
9436
  const offer = !canRead ? page.upsell?.offer : void 0;
8829
9437
  const tier = offer ? authority.get(offer.tierId) : void 0;
8830
9438
  const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
@@ -8837,8 +9445,11 @@ function projectMemberContent(content, viewer, tiers) {
8837
9445
  audience: page.audience,
8838
9446
  source: page.source,
8839
9447
  access: canRead ? "allowed" : "locked",
9448
+ kind: page.kind ?? "markdown",
9449
+ ...page.placeholder ? { placeholder: page.placeholder } : {},
9450
+ ...page.feature ? { feature: page.feature } : {},
8840
9451
  ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
8841
- ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
9452
+ ...canRead && page.bodyMarkdown !== void 0 ? { bodyMarkdown: page.bodyMarkdown } : {},
8842
9453
  ...upsell ? { upsell } : {}
8843
9454
  };
8844
9455
  })
@@ -8850,33 +9461,33 @@ var control = (row2) => row2 && row2.document && typeof row2.document === "objec
8850
9461
  var local2 = (row2) => row2 && row2.document && typeof row2.document === "object" ? row2 : null;
8851
9462
  async function exists(db, ns, where) {
8852
9463
  if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
8853
- const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
8854
- return rows5.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
9464
+ const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
9465
+ return rows6.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
8855
9466
  }
8856
9467
  async function loadActiveMemberContentControl(db, targetId, environment) {
8857
9468
  const where = { targetId, environment, active: true };
8858
9469
  if (!await exists(db, "memberContentControls", where)) return null;
8859
- const rows5 = (await db.query({ memberContentControls: { $: {
9470
+ const rows6 = (await db.query({ memberContentControls: { $: {
8860
9471
  where,
8861
9472
  order: { revision: "desc" },
8862
9473
  limit: 1
8863
9474
  } } })).memberContentControls;
8864
- return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
9475
+ return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
8865
9476
  }
8866
9477
  async function loadLatestMemberContentControl(db, targetId, environment) {
8867
9478
  const where = { targetId, environment };
8868
9479
  if (!await exists(db, "memberContentControls", where)) return null;
8869
- const rows5 = (await db.query({ memberContentControls: { $: {
9480
+ const rows6 = (await db.query({ memberContentControls: { $: {
8870
9481
  where,
8871
9482
  order: { revision: "desc" },
8872
9483
  limit: 1
8873
9484
  } } })).memberContentControls;
8874
- return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
9485
+ return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
8875
9486
  }
8876
9487
  async function loadLocalMemberContent(db, chapterId, environment) {
8877
9488
  const id7 = `member-content-local:${environment}`;
8878
- const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
8879
- return local2(Array.isArray(rows5) ? rows5[0] : void 0);
9489
+ const rows6 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
9490
+ return local2(Array.isArray(rows6) ? rows6[0] : void 0);
8880
9491
  }
8881
9492
  async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
8882
9493
  const { document, digest } = envelope;
@@ -8923,7 +9534,8 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
8923
9534
  var ADMIN_PATH = "/api/admin/member-content";
8924
9535
  var BODY_CAP = 1e6;
8925
9536
  var reject4 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
8926
- async function groupAndTiers(db, chapterId) {
9537
+ async function groupAndTiers(db, chapterId, mode = "chapter") {
9538
+ if (mode === "hub") return { group: null, tiers: [] };
8927
9539
  const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
8928
9540
  const group = Array.isArray(groups) ? groups[0] : void 0;
8929
9541
  if (!group) return { group: null, tiers: [] };
@@ -8960,7 +9572,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
8960
9572
  if (document.source.appId !== settings.sourceId) return reject4("wrong_source", 403);
8961
9573
  if (document.target?.chapterId !== ctx.chapter.id) return reject4("wrong_target", 409);
8962
9574
  if (document.target.environment !== env.ODLA_ENV) return reject4("wrong_environment", 409);
8963
- const { tiers } = await groupAndTiers(db, ctx.chapter.id);
9575
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode);
8964
9576
  const checked = validateMemberContentDocument(document, {
8965
9577
  sourceType: "parent",
8966
9578
  sourceAppId: settings.sourceId,
@@ -9014,17 +9626,13 @@ var handleMemberContent = async (req, url, env, ctx) => {
9014
9626
  const user = await ctx.verifyUser(req, env);
9015
9627
  if (!user) return json({ error: "unauthorized" }, 401);
9016
9628
  const db = ctx.makeDb(env);
9017
- let tierId;
9018
- try {
9019
- const member = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, { userId: user.userId, email: user.email });
9020
- if (!member || !await chapterMembershipAccess(db, env, ctx, member.source)) return json({ error: "member access required" }, 403);
9021
- tierId = typeof member.source.tierId === "string" ? member.source.tierId : null;
9022
- } catch {
9023
- return json({ error: "membership access unavailable" }, 503);
9024
- }
9629
+ const resolved = await resolvePortalMember(db, env, ctx, user);
9630
+ if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
9631
+ if (resolved.status !== "member") return json({ error: "member access required" }, 403);
9632
+ const tierId = resolved.member.tierId;
9025
9633
  const [{ parent, chapter }, { group, tiers }] = await Promise.all([
9026
9634
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
9027
- groupAndTiers(db, ctx.chapter.id)
9635
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
9028
9636
  ]);
9029
9637
  const effective = composeMemberContent(parent?.document, chapter?.document);
9030
9638
  let offerable = tiers;
@@ -9043,7 +9651,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9043
9651
  if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
9044
9652
  const [{ parent, chapter }, { tiers }] = await Promise.all([
9045
9653
  activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
9046
- groupAndTiers(db, ctx.chapter.id)
9654
+ groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
9047
9655
  ]);
9048
9656
  if (req.method === "GET") {
9049
9657
  const effective = composeMemberContent(parent?.document, chapter?.document);
@@ -9051,7 +9659,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9051
9659
  parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
9052
9660
  chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
9053
9661
  effective,
9054
- tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
9662
+ tiers: tiers.map(({ id: id7, name: name2, priceCents, active }) => ({ id: id7, name: name2, priceCents, active })),
9055
9663
  localAuthoring: ctx.chapter.memberContent.localAuthoring
9056
9664
  });
9057
9665
  }
@@ -9078,6 +9686,8 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
9078
9686
  parentPages: parent?.document.pages ?? []
9079
9687
  });
9080
9688
  if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
9689
+ const tiered = ctx.chapter.mode === "hub" ? checked.value.pages.findIndex((page) => page.audience.kind === "tiers") : -1;
9690
+ if (tiered >= 0) return json({ error: "invalid member content", errors: [`document.pages[${tiered}].audience must be all-members: a hub has no tiers`] }, 400);
9081
9691
  try {
9082
9692
  const saved = await saveLocalMemberContent(
9083
9693
  db,
@@ -9099,13 +9709,521 @@ function withLocalAuthority(value, revision, generatedAt, appId) {
9099
9709
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
9100
9710
  return {
9101
9711
  ...value,
9102
- schemaVersion: 1,
9712
+ // The author chooses the contract version; anything else defaults to 1.
9713
+ schemaVersion: value.schemaVersion === 2 ? 2 : 1,
9103
9714
  revision,
9104
9715
  generatedAt,
9105
9716
  source: { type: "chapter", appId }
9106
9717
  };
9107
9718
  }
9108
9719
 
9720
+ // src/portal-http.ts
9721
+ async function readJsonObject(req, maxBytes) {
9722
+ const text11 = await readBoundedRequestText(req, maxBytes);
9723
+ if (text11 instanceof Response) return text11;
9724
+ try {
9725
+ const value = JSON.parse(text11);
9726
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
9727
+ } catch {
9728
+ }
9729
+ return json({ error: "invalid JSON body" }, 400);
9730
+ }
9731
+
9732
+ // src/worker-routes-hub-members.ts
9733
+ var HUB_MEMBERS_PATH = "/api/admin/hub-members";
9734
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9735
+ async function grantId(emailNormalized) {
9736
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(emailNormalized));
9737
+ return `hub-member:${Array.from(new Uint8Array(digest).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
9738
+ }
9739
+ var view = (row2) => ({
9740
+ id: row2.id,
9741
+ email: row2.emailNormalized,
9742
+ firstName: row2.firstName ?? null,
9743
+ lastName: row2.lastName ?? null,
9744
+ status: row2.status,
9745
+ grantedBy: row2.grantedBy,
9746
+ grantedAt: row2.grantedAt,
9747
+ revokedBy: row2.revokedBy ?? null,
9748
+ revokedAt: row2.revokedAt ?? null,
9749
+ revision: row2.revision
9750
+ });
9751
+ var name = (value) => typeof value === "string" && value.trim() ? value.trim().slice(0, 80) : void 0;
9752
+ var handleAdminHubMembers = async (req, url, env, ctx) => {
9753
+ if (url.pathname !== HUB_MEMBERS_PATH && !url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) return null;
9754
+ if (ctx.chapter.mode !== "hub" || !servesMembers(ctx)) return json({ error: "not found" }, 404);
9755
+ const user = await ctx.verifyUser(req, env);
9756
+ if (!user) return json({ error: "unauthorized" }, 401);
9757
+ const db = ctx.makeDb(env);
9758
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
9759
+ const actor = user.email ?? user.userId;
9760
+ if (req.method === "GET" && url.pathname === HUB_MEMBERS_PATH) {
9761
+ const rows6 = (await db.query({ hubMemberGrants: { $: { limit: 1e3 } } })).hubMemberGrants ?? [];
9762
+ return json({ members: rows6.map(view).sort((a, b) => Number(b.grantedAt) - Number(a.grantedAt)) });
9763
+ }
9764
+ if (req.method === "POST" && url.pathname === HUB_MEMBERS_PATH) {
9765
+ const body = await readJsonObject(req, 8192);
9766
+ if (body instanceof Response) return body;
9767
+ const email = typeof body.email === "string" ? body.email.trim() : "";
9768
+ if (!EMAIL.test(email)) return json({ error: "a valid email is required" }, 400);
9769
+ const emailNormalized = normalizeChapterEmail(email);
9770
+ const id7 = await grantId(emailNormalized);
9771
+ const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
9772
+ if (current?.status === "active") return json({ member: view(current), status: "already-active" });
9773
+ const revision = Number(current?.revision ?? 0) + 1;
9774
+ const attrs = {
9775
+ id: id7,
9776
+ emailNormalized,
9777
+ status: "active",
9778
+ grantedBy: actor,
9779
+ grantedAt: Date.now(),
9780
+ revision,
9781
+ ...name(body.firstName) ? { firstName: name(body.firstName) } : {},
9782
+ ...name(body.lastName) ? { lastName: name(body.lastName) } : {}
9783
+ };
9784
+ const ops = [{ t: "update", ns: "hubMemberGrants", id: id7, attrs }];
9785
+ if (current) ops.push({ t: "retract", ns: "hubMemberGrants", id: id7, attrs: ["revokedBy", "revokedAt"] });
9786
+ try {
9787
+ await db.transact(ops, { mutationId: `hub-member-grant:${id7}:${revision}`, guards: [current ? { ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } } : { ns: "hubMemberGrants", id: id7, exists: false }] });
9788
+ } catch {
9789
+ return json({ error: "the roster changed; reload and try again" }, 409);
9790
+ }
9791
+ return json({ member: view({ ...current, ...attrs, revokedBy: void 0, revokedAt: void 0 }), status: current ? "re-granted" : "granted" }, current ? 200 : 201);
9792
+ }
9793
+ if (req.method === "DELETE" && url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) {
9794
+ const id7 = decodeURIComponent(url.pathname.slice(HUB_MEMBERS_PATH.length + 1));
9795
+ const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
9796
+ if (!current) return json({ error: "not found" }, 404);
9797
+ if (current.status !== "active") return json({ member: view(current), status: "already-revoked" });
9798
+ const attrs = { status: "revoked", revokedBy: actor, revokedAt: Date.now(), revision: Number(current.revision) + 1 };
9799
+ try {
9800
+ await db.transact([{ t: "update", ns: "hubMemberGrants", id: id7, attrs }], {
9801
+ mutationId: `hub-member-revoke:${id7}:${attrs.revision}`,
9802
+ guards: [{ ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } }]
9803
+ });
9804
+ } catch {
9805
+ return json({ error: "the roster changed; reload and try again" }, 409);
9806
+ }
9807
+ return json({ member: view({ ...current, ...attrs }), status: "revoked" });
9808
+ }
9809
+ return json({ error: "method not allowed" }, 405);
9810
+ };
9811
+
9812
+ // src/image-metadata.ts
9813
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
9814
+ var PNG_KEEP = /* @__PURE__ */ new Set(["IHDR", "PLTE", "IDAT", "IEND", "tRNS", "gAMA", "cHRM", "sRGB", "sBIT", "bKGD", "pHYs"]);
9815
+ var JPEG_KEEP = (marker) => marker === 224 || marker === 238 || marker === 219 || marker === 196 || marker === 221 || marker >= 192 && marker <= 207 && marker !== 200;
9816
+ var u16 = (b, i) => b[i] << 8 | b[i + 1];
9817
+ var u32 = (b, i) => (b[i] << 24 >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
9818
+ function concat(parts) {
9819
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
9820
+ let offset = 0;
9821
+ for (const part of parts) {
9822
+ out.set(part, offset);
9823
+ offset += part.length;
9824
+ }
9825
+ return out;
9826
+ }
9827
+ function cleanPng(bytes) {
9828
+ const parts = [bytes.subarray(0, 8)];
9829
+ let width = 0, height = 0, offset = 8, ended = false;
9830
+ while (offset + 12 <= bytes.length) {
9831
+ const length = u32(bytes, offset);
9832
+ const end = offset + 12 + length;
9833
+ if (end > bytes.length) return null;
9834
+ const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
9835
+ if (type === "IHDR") {
9836
+ width = u32(bytes, offset + 8);
9837
+ height = u32(bytes, offset + 12);
9838
+ }
9839
+ if (PNG_KEEP.has(type)) parts.push(bytes.subarray(offset, end));
9840
+ offset = end;
9841
+ if (type === "IEND") {
9842
+ ended = true;
9843
+ break;
9844
+ }
9845
+ }
9846
+ if (!ended || !width || !height) return null;
9847
+ return { contentType: "image/png", extension: "png", width, height, bytes: concat(parts) };
9848
+ }
9849
+ function cleanJpeg(bytes) {
9850
+ const parts = [bytes.subarray(0, 2)];
9851
+ let width = 0, height = 0, offset = 2;
9852
+ while (offset + 4 <= bytes.length) {
9853
+ if (bytes[offset] !== 255) return null;
9854
+ const marker = bytes[offset + 1];
9855
+ if (marker === 255) {
9856
+ offset += 1;
9857
+ continue;
9858
+ }
9859
+ const length = u16(bytes, offset + 2);
9860
+ const end = offset + 2 + length;
9861
+ if (length < 2 || end > bytes.length) return null;
9862
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
9863
+ height = u16(bytes, offset + 5);
9864
+ width = u16(bytes, offset + 7);
9865
+ }
9866
+ if (marker === 218) {
9867
+ parts.push(bytes.subarray(offset));
9868
+ return width && height ? { contentType: "image/jpeg", extension: "jpg", width, height, bytes: concat(parts) } : null;
9869
+ }
9870
+ if (JPEG_KEEP(marker)) parts.push(bytes.subarray(offset, end));
9871
+ offset = end;
9872
+ }
9873
+ return null;
9874
+ }
9875
+ function cleanImage(bytes) {
9876
+ if (bytes.length > 8 && PNG_SIGNATURE.every((byte, index2) => bytes[index2] === byte)) return cleanPng(bytes);
9877
+ if (bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return cleanJpeg(bytes);
9878
+ return null;
9879
+ }
9880
+
9881
+ // src/member-profile.ts
9882
+ var MEMBER_PROFILE_FIELDS = {
9883
+ preferredName: "person.preferredName",
9884
+ location: "person.region",
9885
+ prompts: "person.profilePrompts",
9886
+ introducedByText: "person.introducedByText"
9887
+ };
9888
+ var MAX_PROMPT_ANSWER = 280;
9889
+ var clean = (value) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
9890
+ function optionalText(body, key3, max, errors) {
9891
+ if (!(key3 in body)) return void 0;
9892
+ const value = body[key3];
9893
+ if (value === null || value === "") return null;
9894
+ if (typeof value !== "string") {
9895
+ errors.push(`${key3} must be a string`);
9896
+ return void 0;
9897
+ }
9898
+ const text11 = clean(value);
9899
+ if (text11.length > max) errors.push(`${key3} must be at most ${max} characters`);
9900
+ return text11 || null;
9901
+ }
9902
+ function validateProfilePatch(body, portal) {
9903
+ const errors = [];
9904
+ for (const key3 of Object.keys(body)) {
9905
+ if (key3 === "expectedRevision") continue;
9906
+ if (!(key3 in MEMBER_PROFILE_FIELDS)) errors.push(`${key3} is not an editable profile field`);
9907
+ else if (!MEMBER_SELF_EDITABLE_FIELDS.has(MEMBER_PROFILE_FIELDS[key3])) errors.push(`${key3} is read-only`);
9908
+ }
9909
+ const patch = {};
9910
+ const preferredName = optionalText(body, "preferredName", 80, errors);
9911
+ if (preferredName !== void 0) patch.preferredName = preferredName;
9912
+ const location = optionalText(body, "location", 80, errors);
9913
+ if (location !== void 0) patch.location = location;
9914
+ const introducedByText = optionalText(body, "introducedByText", 120, errors);
9915
+ if (introducedByText !== void 0) patch.introducedByText = introducedByText;
9916
+ if ("prompts" in body) {
9917
+ const raw2 = body.prompts;
9918
+ if (!Array.isArray(raw2)) errors.push("prompts must be an array");
9919
+ else {
9920
+ if (raw2.length > portal.maxPrompts) errors.push(`prompts supports at most ${portal.maxPrompts} answers`);
9921
+ const seen = /* @__PURE__ */ new Set();
9922
+ const prompts = [];
9923
+ for (const [index2, item] of raw2.entries()) {
9924
+ const entry = item && typeof item === "object" ? item : {};
9925
+ const prompt = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
9926
+ const answer = typeof entry.answer === "string" ? clean(entry.answer) : "";
9927
+ if (!portal.profilePrompts.includes(prompt)) errors.push(`prompts[${index2}].prompt is not one of this site's prompts`);
9928
+ if (seen.has(prompt)) errors.push(`prompts[${index2}].prompt is already answered`);
9929
+ if (entry.answer !== void 0 && typeof entry.answer !== "string") errors.push(`prompts[${index2}].answer must be a string`);
9930
+ if (answer.length > MAX_PROMPT_ANSWER) errors.push(`prompts[${index2}].answer must be at most ${MAX_PROMPT_ANSWER} characters`);
9931
+ seen.add(prompt);
9932
+ prompts.push({ prompt, answer });
9933
+ }
9934
+ patch.prompts = prompts;
9935
+ }
9936
+ }
9937
+ return errors.length ? { ok: false, errors } : { ok: true, patch };
9938
+ }
9939
+ var nameKey = (value) => value.toLowerCase().normalize("NFKC").replace(/[^\p{L}\p{N}\s'-]/gu, " ").replace(/\s+/g, " ").trim();
9940
+
9941
+ // src/member-profile-store.ts
9942
+ async function loadMemberProfile(db, memberId) {
9943
+ return (await db.query({ memberProfiles: { $: { where: { id: memberId }, limit: 1 } } })).memberProfiles?.[0] ?? null;
9944
+ }
9945
+ async function membersNamed(db, mode, chapterId, text11, selfId) {
9946
+ const wanted = nameKey(text11);
9947
+ if (!wanted) return [];
9948
+ const rows6 = mode === "hub" ? (await db.query({ hubMemberGrants: { $: { where: { status: "active" }, limit: 5e3 } } })).hubMemberGrants ?? [] : ((await db.query({ applications: { $: { where: { groupId: chapterId, status: "approved" }, limit: 5e3 } } })).applications ?? []).filter((row2) => row2.canceled !== true);
9949
+ return rows6.filter((row2) => String(row2.id) !== selfId && nameKey(`${row2.firstName ?? ""} ${row2.lastName ?? ""}`) === wanted).map((row2) => String(row2.id));
9950
+ }
9951
+ function reviewItemOp(id7, kind, detail, memberId, relatedMemberId) {
9952
+ return { t: "update", ns: "portalReviewItems", id: id7, attrs: {
9953
+ id: id7,
9954
+ kind,
9955
+ detail,
9956
+ status: "open",
9957
+ createdAt: Date.now(),
9958
+ ...memberId ? { memberId } : {},
9959
+ ...relatedMemberId ? { relatedMemberId } : {}
9960
+ } };
9961
+ }
9962
+ async function saveMemberProfile(db, input) {
9963
+ const current = await loadMemberProfile(db, input.memberId);
9964
+ if (Number(current?.revision ?? 0) !== input.expectedRevision) throw new Error("member_profile_revision_conflict");
9965
+ const now = Date.now();
9966
+ const revision = input.expectedRevision + 1;
9967
+ const attrs = { id: input.memberId, memberId: input.memberId, revision, updatedAt: now };
9968
+ const retract = [];
9969
+ const set = (key3, value) => {
9970
+ if (value === null) retract.push(key3);
9971
+ else attrs[key3] = value;
9972
+ };
9973
+ if (input.patch.preferredName !== void 0) set("preferredName", input.patch.preferredName);
9974
+ if (input.patch.location !== void 0) set("location", input.patch.location);
9975
+ if (input.patch.prompts !== void 0) attrs.prompts = input.patch.prompts;
9976
+ const ops = [];
9977
+ let review = null;
9978
+ const introduced = input.patch.introducedByText;
9979
+ if (introduced !== void 0 && introduced !== (current?.introducedByText ?? null)) {
9980
+ set("introducedByText", introduced);
9981
+ if (introduced === null) retract.push("introducedByMemberId", "introducedAt");
9982
+ else {
9983
+ const matches2 = await membersNamed(db, input.mode, input.chapterId, introduced, input.memberId);
9984
+ if (matches2.length === 1) {
9985
+ Object.assign(attrs, { introducedByMemberId: matches2[0], introducedAt: now });
9986
+ review = "matched";
9987
+ } else {
9988
+ retract.push("introducedByMemberId", "introducedAt");
9989
+ ops.push(reviewItemOp(
9990
+ `introduction:${input.memberId}:${revision}`,
9991
+ "introduction_unmatched",
9992
+ { text: introduced, candidates: matches2.length },
9993
+ input.memberId
9994
+ ));
9995
+ review = "queued";
9996
+ }
9997
+ }
9998
+ }
9999
+ ops.unshift({ t: "update", ns: "memberProfiles", id: input.memberId, attrs });
10000
+ const absent = retract.filter((key3) => current && current[key3] !== void 0);
10001
+ if (absent.length) ops.splice(1, 0, { t: "retract", ns: "memberProfiles", id: input.memberId, attrs: absent });
10002
+ await db.transact(ops, {
10003
+ mutationId: `member-profile:${input.memberId}:${revision}`,
10004
+ guards: [current ? { ns: "memberProfiles", id: input.memberId, exists: true, equals: { revision: input.expectedRevision } } : { ns: "memberProfiles", id: input.memberId, exists: false }]
10005
+ });
10006
+ const row2 = { ...current, ...attrs };
10007
+ for (const key3 of absent) delete row2[key3];
10008
+ return { row: row2, review };
10009
+ }
10010
+
10011
+ // src/worker-routes-member-profile.ts
10012
+ var MEMBER_PROFILE_PATH = "/api/member/profile";
10013
+ var MEMBER_PHOTO_PATH = "/api/member/profile/photo";
10014
+ var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
10015
+ var PHOTO_SIZE = 400;
10016
+ async function portalMemberGate(req, env, ctx) {
10017
+ const user = await ctx.verifyUser(req, env);
10018
+ if (!user) return json({ error: "unauthorized" }, 401);
10019
+ const db = ctx.makeDb(env);
10020
+ const resolved = await resolvePortalMember(db, env, ctx, user);
10021
+ if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
10022
+ if (resolved.status !== "member") return json({ error: "member access required" }, 403);
10023
+ return { db, member: resolved.member };
10024
+ }
10025
+ async function profileView(db, ctx, member) {
10026
+ const row2 = await loadMemberProfile(db, member.memberId);
10027
+ const portal = ctx.chapter.memberPortal;
10028
+ return {
10029
+ revision: Number(row2?.revision ?? 0),
10030
+ identity: await portalIdentity(db, ctx, member),
10031
+ profile: {
10032
+ preferredName: row2?.preferredName ?? null,
10033
+ location: row2?.location ?? null,
10034
+ prompts: Array.isArray(row2?.prompts) ? row2.prompts : [],
10035
+ introducedByText: row2?.introducedByText ?? null,
10036
+ introducedByMatched: typeof row2?.introducedByMemberId === "string"
10037
+ },
10038
+ promptChoices: portal.profilePrompts,
10039
+ maxPrompts: portal.maxPrompts,
10040
+ photos: portal.photos
10041
+ };
10042
+ }
10043
+ var handleMemberProfile = async (req, url, env, ctx) => {
10044
+ if (url.pathname !== MEMBER_PROFILE_PATH && url.pathname !== MEMBER_PHOTO_PATH) return null;
10045
+ const portal = ctx.chapter.memberPortal;
10046
+ if (!portal) return json({ error: "not found" }, 404);
10047
+ const gate12 = await portalMemberGate(req, env, ctx);
10048
+ if (gate12 instanceof Response) return gate12;
10049
+ const { db, member } = gate12;
10050
+ if (url.pathname === MEMBER_PHOTO_PATH) return uploadPhoto(req, db, ctx, member);
10051
+ if (req.method === "GET") return json(await profileView(db, ctx, member));
10052
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
10053
+ const body = await readJsonObject(req, 16384);
10054
+ if (body instanceof Response) return body;
10055
+ const expectedRevision = body.expectedRevision;
10056
+ if (!Number.isInteger(expectedRevision) || Number(expectedRevision) < 0) {
10057
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
10058
+ }
10059
+ const checked = validateProfilePatch(body, portal);
10060
+ if (!checked.ok) return json({ error: "invalid profile", errors: checked.errors }, 400);
10061
+ try {
10062
+ const saved = await saveMemberProfile(db, {
10063
+ mode: ctx.chapter.mode,
10064
+ chapterId: ctx.chapter.id,
10065
+ memberId: member.memberId,
10066
+ patch: checked.patch,
10067
+ expectedRevision: Number(expectedRevision)
10068
+ });
10069
+ return json({ ...await profileView(db, ctx, member), introducedBy: saved.review });
10070
+ } catch (error) {
10071
+ if (error instanceof Error && error.message === "member_profile_revision_conflict") {
10072
+ return json({ error: "your profile changed elsewhere; reload before saving" }, 409);
10073
+ }
10074
+ return json({ error: "profile could not be saved" }, 503);
10075
+ }
10076
+ };
10077
+ async function uploadPhoto(req, db, ctx, member) {
10078
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
10079
+ if (!ctx.chapter.memberPortal?.photos || !db.storage) return json({ error: "photos are not enabled" }, 404);
10080
+ const declared = Number(req.headers.get("content-length") ?? 0);
10081
+ if (declared > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
10082
+ const bytes = new Uint8Array(await req.arrayBuffer());
10083
+ if (bytes.length > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
10084
+ const image = cleanImage(bytes);
10085
+ if (!image) return json({ error: "photo must be a JPEG or PNG" }, 415);
10086
+ if (image.width > PHOTO_SIZE || image.height > PHOTO_SIZE) {
10087
+ return json({ error: `photo must be at most ${PHOTO_SIZE} by ${PHOTO_SIZE} pixels` }, 422);
10088
+ }
10089
+ const now = Date.now();
10090
+ const path = `member-photos/${encodeURIComponent(member.memberId)}/${now}.${image.extension}`;
10091
+ await db.storage.upload(path, new Blob([image.bytes], { type: image.contentType }), image.contentType, { private: true });
10092
+ const previous = await loadMemberProfile(db, member.memberId);
10093
+ await db.transact([{ t: "update", ns: "memberProfiles", id: member.memberId, attrs: {
10094
+ id: member.memberId,
10095
+ memberId: member.memberId,
10096
+ photoPath: path,
10097
+ photoUpdatedAt: now,
10098
+ updatedAt: now,
10099
+ ...previous ? {} : { revision: 0 }
10100
+ } }], { mutationId: `member-photo:${member.memberId}:${now}` });
10101
+ const old = typeof previous?.photoPath === "string" ? previous.photoPath : null;
10102
+ if (old && old !== path) await db.storage.delete(old).catch(() => void 0);
10103
+ return json({ photoUrl: await db.storage.sign(path, 3600), width: image.width, height: image.height });
10104
+ }
10105
+
10106
+ // src/worker-routes-admin-member-tier.ts
10107
+ var PATH5 = /^\/api\/admin\/applications\/([^/]+)\/entitlement-tier$/;
10108
+ var handleAdminMemberTier = async (req, url, env, ctx) => {
10109
+ const match = PATH5.exec(url.pathname);
10110
+ if (!match) return null;
10111
+ if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
10112
+ if (!["GET", "POST", "DELETE"].includes(req.method)) return json({ error: "method not allowed" }, 405);
10113
+ const user = await ctx.verifyUser(req, env);
10114
+ if (!user) return json({ error: "unauthorized" }, 401);
10115
+ const db = ctx.makeDb(env);
10116
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
10117
+ const id7 = decodeURIComponent(match[1]);
10118
+ const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
10119
+ if (!app || app.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
10120
+ const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
10121
+ const tiers = group ? await loadTiers(db, group) : [];
10122
+ if (req.method === "GET") {
10123
+ const history = (await db.query({ memberTierChanges: { $: { where: { applicationId: id7 }, limit: 50 } } })).memberTierChanges ?? [];
10124
+ return json({
10125
+ purchasedTierId: app.tierId ?? null,
10126
+ entitledTierId: entitledTierId(app),
10127
+ entitlementTierEndsAt: app.entitlementTierEndsAt ?? null,
10128
+ tiers: tiers.filter((tier) => tier.active !== false).map(({ id: tierId, name: name2 }) => ({ id: tierId, name: name2 })),
10129
+ history: history.sort((a, b) => Number(b.at) - Number(a.at))
10130
+ });
10131
+ }
10132
+ const body = await readJsonObject(req, 8192);
10133
+ if (body instanceof Response) return body;
10134
+ if (app.status !== "approved" || app.canceled === true) return json({ error: "only an approved member can change tier" }, 409);
10135
+ const reason = typeof body.reason === "string" ? body.reason.trim() : "";
10136
+ if (!reason || reason.length > 500) return json({ error: "a reason of up to 500 characters is required" }, 400);
10137
+ const paymentReference = typeof body.paymentReference === "string" && body.paymentReference.trim() ? body.paymentReference.trim().slice(0, 200) : void 0;
10138
+ const now = Date.now();
10139
+ let toTierId;
10140
+ let endsAt;
10141
+ if (req.method === "POST") {
10142
+ toTierId = typeof body.tierId === "string" ? body.tierId : "";
10143
+ const tier = tiers.find((candidate) => candidate.id === toTierId);
10144
+ if (!tier || tier.active === false) return json({ error: "tierId must be an active tier" }, 400);
10145
+ if (body.endsAt !== void 0) {
10146
+ endsAt = Number(body.endsAt);
10147
+ if (!Number.isSafeInteger(endsAt) || endsAt <= now) return json({ error: "endsAt must be a future timestamp" }, 400);
10148
+ }
10149
+ } else if (app.entitlementTierId === void 0) return json({ error: "no entitlement tier to revert" }, 409);
10150
+ const fromTierId = entitledTierId(app, now) ?? void 0;
10151
+ const changeId = `tier-change:${id7}:${now}`;
10152
+ const ops = [
10153
+ { t: "update", ns: "memberTierChanges", id: changeId, attrs: {
10154
+ id: changeId,
10155
+ applicationId: id7,
10156
+ reason,
10157
+ actor: user.email ?? user.userId,
10158
+ at: now,
10159
+ ...fromTierId ? { fromTierId } : {},
10160
+ ...toTierId ? { toTierId } : {},
10161
+ ...endsAt ? { endsAt } : {},
10162
+ ...paymentReference ? { paymentReference } : {}
10163
+ } },
10164
+ toTierId ? { t: "update", ns: "applications", id: id7, attrs: { entitlementTierId: toTierId, ...endsAt ? { entitlementTierEndsAt: endsAt } : {} } } : { t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierId", "entitlementTierEndsAt"] }
10165
+ ];
10166
+ if (toTierId && !endsAt && app.entitlementTierEndsAt !== void 0) ops.push({ t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierEndsAt"] });
10167
+ const current = app.entitlementTierId;
10168
+ try {
10169
+ await db.transact(ops, { mutationId: changeId, guards: [current === void 0 ? { ns: "applications", id: id7, exists: true, equals: { status: "approved" }, absent: ["entitlementTierId"] } : { ns: "applications", id: id7, exists: true, equals: { status: "approved", entitlementTierId: current } }] });
10170
+ } catch {
10171
+ return json({ error: "this member changed; reload and try again" }, 409);
10172
+ }
10173
+ return json({ ok: true, applicationId: id7, fromTierId: fromTierId ?? null, toTierId: toTierId ?? (typeof app.tierId === "string" ? app.tierId : null), endsAt: endsAt ?? null });
10174
+ };
10175
+
10176
+ // src/worker-routes-admin-portal-review.ts
10177
+ var PORTAL_REVIEW_PATH = "/api/admin/portal-review";
10178
+ var handleAdminPortalReview = async (req, url, env, ctx) => {
10179
+ if (url.pathname !== PORTAL_REVIEW_PATH && !url.pathname.startsWith(`${PORTAL_REVIEW_PATH}/`)) return null;
10180
+ if (!ctx.chapter.memberPortal) return json({ error: "not found" }, 404);
10181
+ const user = await ctx.verifyUser(req, env);
10182
+ if (!user) return json({ error: "unauthorized" }, 401);
10183
+ const db = ctx.makeDb(env);
10184
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
10185
+ if (req.method === "GET" && url.pathname === PORTAL_REVIEW_PATH) {
10186
+ const status2 = url.searchParams.get("status") ?? "open";
10187
+ const where = status2 === "all" ? {} : { status: status2 };
10188
+ const items = (await db.query({ portalReviewItems: { $: { where, limit: 1e3 } } })).portalReviewItems ?? [];
10189
+ return json({ items: items.sort((a, b) => Number(b.createdAt) - Number(a.createdAt)) });
10190
+ }
10191
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
10192
+ const id7 = decodeURIComponent(url.pathname.slice(PORTAL_REVIEW_PATH.length + 1));
10193
+ const body = await readJsonObject(req, 4096);
10194
+ if (body instanceof Response) return body;
10195
+ const decision = typeof body.decision === "string" ? body.decision.trim() : "";
10196
+ if (!decision || decision.length > 1e3) return json({ error: "a decision of up to 1,000 characters is required" }, 400);
10197
+ const item = (await db.query({ portalReviewItems: { $: { where: { id: id7 }, limit: 1 } } })).portalReviewItems?.[0];
10198
+ if (!item) return json({ error: "not found" }, 404);
10199
+ if (item.status !== "open") return json({ error: "already resolved" }, 409);
10200
+ const attrs = { status: "resolved", decision, resolvedBy: user.email ?? user.userId, resolvedAt: Date.now() };
10201
+ try {
10202
+ await db.transact([{ t: "update", ns: "portalReviewItems", id: id7, attrs }], {
10203
+ mutationId: `portal-review:${id7}:resolve`,
10204
+ guards: [{ ns: "portalReviewItems", id: id7, exists: true, equals: { status: "open" } }]
10205
+ });
10206
+ } catch {
10207
+ return json({ error: "already resolved" }, 409);
10208
+ }
10209
+ return json({ item: { ...item, ...attrs } });
10210
+ };
10211
+
10212
+ // src/worker-routes-member-portal.ts
10213
+ var PORTAL_ROUTES = [
10214
+ handleMemberProfile,
10215
+ handleAdminHubMembers,
10216
+ handleAdminMemberTier,
10217
+ handleAdminPortalReview
10218
+ ];
10219
+ var handleMemberPortal = async (req, url, env, ctx) => {
10220
+ for (const route of PORTAL_ROUTES) {
10221
+ const response = await route(req, url, env, ctx);
10222
+ if (response) return response;
10223
+ }
10224
+ return null;
10225
+ };
10226
+
9109
10227
  // src/worker-routes-admission-grants.ts
9110
10228
  var import_db5 = require("@odla-ai/db");
9111
10229
  var reject5 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
@@ -9486,9 +10604,9 @@ function integer3(value, fallback, min, max) {
9486
10604
  const parsed = Number(value);
9487
10605
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
9488
10606
  }
9489
- function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
10607
+ function projectRecord(record4, allowed, editable, stagesEnabled, crm) {
9490
10608
  const fields = Object.fromEntries(
9491
- allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
10609
+ allowed.flatMap((name2) => Object.prototype.hasOwnProperty.call(record4.fields, name2) ? [[name2, record4.fields[name2]]] : [])
9492
10610
  );
9493
10611
  const def = crm.type(record4.type);
9494
10612
  const editFields = editable.map((id7) => {
@@ -9598,7 +10716,7 @@ async function deliverAdmissionGrants(options) {
9598
10716
  }
9599
10717
 
9600
10718
  // src/worker-routes-network-admission.ts
9601
- var PATH5 = "/api/admin/network/admissions";
10719
+ var PATH6 = "/api/admin/network/admissions";
9602
10720
  var RETRY_PATH = "/api/admin/network/admissions/retry";
9603
10721
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
9604
10722
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -9678,11 +10796,11 @@ async function parseBody(req) {
9678
10796
  }
9679
10797
  }
9680
10798
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
9681
- if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
10799
+ if (![PATH6, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
9682
10800
  const auth = await gate2(req, env, ctx);
9683
10801
  if ("response" in auth) return auth.response;
9684
10802
  const db = auth.db;
9685
- if (url.pathname === PATH5 && req.method === "GET") {
10803
+ if (url.pathname === PATH6 && req.method === "GET") {
9686
10804
  const targetId2 = url.searchParams.get("targetId") ?? "";
9687
10805
  const target2 = targetFor(ctx, targetId2);
9688
10806
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -9830,8 +10948,8 @@ function makeCalendar(env) {
9830
10948
  }
9831
10949
  async function firstRow3(db, ns, q) {
9832
10950
  const res = await db.query({ [ns]: { $: q } });
9833
- const rows5 = res[ns];
9834
- return Array.isArray(rows5) ? rows5[0] : void 0;
10951
+ const rows6 = res[ns];
10952
+ return Array.isArray(rows6) ? rows6[0] : void 0;
9835
10953
  }
9836
10954
  async function computeSlots(cal, cfg) {
9837
10955
  const { from, to } = slotWindow(Date.now(), cfg.windowDays);
@@ -10690,18 +11808,18 @@ async function startSubscription(req, env, ctx) {
10690
11808
 
10691
11809
  // src/worker-payment-receipt.ts
10692
11810
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10693
- var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
11811
+ var escape = (text11) => text11.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10694
11812
  async function paymentReceiptRoute(req, url, env, ctx) {
10695
11813
  const id7 = url.searchParams.get("application");
10696
11814
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
10697
11815
  if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
10698
11816
  const db = ctx.makeDb(env);
10699
- const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
10700
- const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
11817
+ const rows6 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
11818
+ const app = rows6.applications?.[0], intent = rows6.subscriptionCheckoutIntents?.[0];
10701
11819
  if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
10702
11820
  const receipt = intent.paymentReceipt;
10703
11821
  if (!receipt) return json2({ pending: true }, 202);
10704
- const text10 = paymentReceiptText(receipt, ctx.chapter.name);
11822
+ const text11 = paymentReceiptText(receipt, ctx.chapter.name);
10705
11823
  if (req.method === "POST") {
10706
11824
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10707
11825
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10719,7 +11837,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10719
11837
  template: "paymentReceipt",
10720
11838
  to: String(intent.purchaserEmailNormalized),
10721
11839
  applicationId: id7,
10722
- vars: { chapterName: ctx.chapter.name, receiptText: text10 },
11840
+ vars: { chapterName: ctx.chapter.name, receiptText: text11 },
10723
11841
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10724
11842
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10725
11843
  });
@@ -10729,7 +11847,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10729
11847
  const nonce = crypto.randomUUID();
10730
11848
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10731
11849
  <style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
10732
- <h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
11850
+ <h1>Payment receipt</h1><pre>${escape(text11)}</pre><button id="print">Print / save PDF</button>
10733
11851
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10734
11852
  headers: {
10735
11853
  "content-type": "text/html; charset=utf-8",
@@ -10762,7 +11880,7 @@ var handlePayments = async (req, url, env, ctx) => {
10762
11880
 
10763
11881
  // src/worker-routes-gifts.ts
10764
11882
  var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
10765
- var rows3 = (value) => Array.isArray(value) ? value : [];
11883
+ var rows4 = (value) => Array.isArray(value) ? value : [];
10766
11884
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
10767
11885
  var userFor = async (req, env, ctx) => {
10768
11886
  const user = await ctx.verifyUser(req, env);
@@ -10800,7 +11918,7 @@ var handleGifts = async (req, url, env, ctx) => {
10800
11918
  const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
10801
11919
  return json({
10802
11920
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
10803
- claims: buyerGiftClaims(rows3(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
11921
+ claims: buyerGiftClaims(rows4(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
10804
11922
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
10805
11923
  merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
10806
11924
  });
@@ -10823,7 +11941,7 @@ var handleGifts = async (req, url, env, ctx) => {
10823
11941
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
10824
11942
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
10825
11943
  const emailNormalized = normalizeChapterEmail(user.email);
10826
- const applications = rows3((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
11944
+ const applications = rows4((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
10827
11945
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
10828
11946
  try {
10829
11947
  return json(await startGiftIntent(db, {
@@ -10953,9 +12071,9 @@ var receiptFor2 = (seat, released) => ({
10953
12071
  });
10954
12072
  async function regiftNamedSeat(db, input, now) {
10955
12073
  membershipClock(now);
10956
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
12074
+ const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
10957
12075
  const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
10958
- if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
12076
+ if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
10959
12077
  const state = await loadMembershipAuthorityState(db, input);
10960
12078
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
10961
12079
  requireMembershipScope(seat, input);
@@ -10964,7 +12082,7 @@ async function regiftNamedSeat(db, input, now) {
10964
12082
  if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
10965
12083
  if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
10966
12084
  const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
10967
- if (seat.recipientEmailNormalized === email && seat.recipientName === name && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
12085
+ if (seat.recipientEmailNormalized === email && seat.recipientName === name2 && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
10968
12086
  return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
10969
12087
  }
10970
12088
  if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
@@ -11002,7 +12120,7 @@ async function regiftNamedSeat(db, input, now) {
11002
12120
  });
11003
12121
  const ops = [
11004
12122
  membershipUpdate("networkSeat", String(seat.id), {
11005
- recipientName: name,
12123
+ recipientName: name2,
11006
12124
  recipientEmailNormalized: email,
11007
12125
  recipientClaimId: claimId,
11008
12126
  status: "pending_acceptance",
@@ -11199,16 +12317,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
11199
12317
  };
11200
12318
 
11201
12319
  // src/named-seat-accepted-notice.ts
11202
- var text7 = (value) => typeof value === "string" ? value : "";
12320
+ var text8 = (value) => typeof value === "string" ? value : "";
11203
12321
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
11204
12322
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
11205
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
12323
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text8(purchaser.email)) {
11206
12324
  return { sent: false, reason: "purchaser-missing" };
11207
12325
  }
11208
12326
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
11209
12327
  if (!group) return { sent: false, reason: "group-missing" };
11210
12328
  const config = emailGroupFrom(group);
11211
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
12329
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text8).join(" ").replace(/\s+/g, " ").trim();
11212
12330
  return sendTemplated({
11213
12331
  db,
11214
12332
  envName: env.ODLA_ENV,
@@ -11220,12 +12338,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
11220
12338
  }, {
11221
12339
  group: config,
11222
12340
  template: "seatAccepted",
11223
- to: text7(purchaser.email),
12341
+ to: text8(purchaser.email),
11224
12342
  dedupeKey: `named-seat-accepted:${input.seatId}`,
11225
12343
  applicationId: input.primaryApplicationId,
11226
12344
  tierId: input.skuId,
11227
12345
  vars: {
11228
- firstName: text7(purchaser.firstName),
12346
+ firstName: text8(purchaser.firstName),
11229
12347
  purchaserName,
11230
12348
  recipientName: input.recipientName,
11231
12349
  tierName: await tierName(db, group, input.skuId),
@@ -11686,8 +12804,8 @@ async function upcomingEvents(env) {
11686
12804
  const res = await cal.availability.upcoming();
11687
12805
  return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
11688
12806
  }
11689
- function toReconcile(rows5) {
11690
- return rows5.map((m) => ({
12807
+ function toReconcile(rows6) {
12808
+ return rows6.map((m) => ({
11691
12809
  id: String(m.id),
11692
12810
  applicationId: String(m.applicationId),
11693
12811
  googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
@@ -11709,12 +12827,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11709
12827
  const query = await db.query({
11710
12828
  meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
11711
12829
  });
11712
- let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
11713
- if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
11714
- if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
12830
+ let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
12831
+ if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
12832
+ if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
11715
12833
  let decisions = [];
11716
12834
  try {
11717
- decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
12835
+ decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
11718
12836
  } catch {
11719
12837
  }
11720
12838
  const patched = /* @__PURE__ */ new Map();
@@ -11741,7 +12859,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
11741
12859
  };
11742
12860
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
11743
12861
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
11744
- const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
12862
+ const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
11745
12863
  return json({ meetings, adopted: decisions.length, timezone });
11746
12864
  };
11747
12865
 
@@ -11880,15 +12998,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11880
12998
  for (const a of data.applications ?? []) {
11881
12999
  const key3 = String(a.email ?? "").toLowerCase();
11882
13000
  if (!key3) continue;
11883
- const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
13001
+ const name2 = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
11884
13002
  const row2 = people.get(key3);
11885
13003
  if (row2) {
11886
13004
  if (!row2.application) row2.application = applicationSummary(a);
11887
- if (!row2.name) row2.name = name;
13005
+ if (!row2.name) row2.name = name2;
11888
13006
  } else {
11889
13007
  people.set(key3, {
11890
13008
  email: String(a.email),
11891
- name,
13009
+ name: name2,
11892
13010
  userId: null,
11893
13011
  role: null,
11894
13012
  profile: null,
@@ -11897,10 +13015,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
11897
13015
  });
11898
13016
  }
11899
13017
  }
11900
- const rows5 = [...people.values()].sort(
13018
+ const rows6 = [...people.values()].sort(
11901
13019
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
11902
13020
  );
11903
- return json({ people: rows5 });
13021
+ return json({ people: rows6 });
11904
13022
  };
11905
13023
  var handleAdminPeopleAccess = async (req, url, env, ctx) => {
11906
13024
  if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
@@ -12101,7 +13219,7 @@ async function gate3(req, env, ctx) {
12101
13219
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
12102
13220
  return rawDb;
12103
13221
  }
12104
- var rows4 = (v) => Array.isArray(v) ? v : [];
13222
+ var rows5 = (v) => Array.isArray(v) ? v : [];
12105
13223
  var handleAdminDashboard = async (req, url, env, ctx) => {
12106
13224
  if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
12107
13225
  const got = await gate3(req, env, ctx);
@@ -12123,8 +13241,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12123
13241
  crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
12124
13242
  groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
12125
13243
  });
12126
- const apps = rows4(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
12127
- const incomplete = rows4(data.applications).length >= 1e3 || rows4(data.crm_record).length >= 1e3 || rows4(data.meetings).length >= 500;
13244
+ const apps = rows5(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
13245
+ const incomplete = rows5(data.applications).length >= 1e3 || rows5(data.crm_record).length >= 1e3 || rows5(data.meetings).length >= 500;
12128
13246
  const applications = {
12129
13247
  total: apps.length,
12130
13248
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -12140,7 +13258,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12140
13258
  for (const a of apps) {
12141
13259
  if (String(a.status) in pipeline) pipeline[String(a.status)] = (pipeline[String(a.status)] ?? 0) + 1;
12142
13260
  }
12143
- for (const r of rows4(data.crm_record)) {
13261
+ for (const r of rows5(data.crm_record)) {
12144
13262
  if (!appById.has(r.fields?.applicationId)) continue;
12145
13263
  const s = r.stage;
12146
13264
  if (s in pipeline) {
@@ -12148,7 +13266,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12148
13266
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
12149
13267
  }
12150
13268
  }
12151
- const meetings = rows4(data.meetings).filter((m) => appById.has(m.applicationId));
13269
+ const meetings = rows5(data.meetings).filter((m) => appById.has(m.applicationId));
12152
13270
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
12153
13271
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
12154
13272
  const agenda = upcoming.slice(0, 8).map((m) => {
@@ -12171,12 +13289,12 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
12171
13289
  const sk = await getVaultSecret(db, "stripe_secret_key");
12172
13290
  if (sk) {
12173
13291
  const subsRes = await chapterBillingSubscriptions(sk, ctx.chapter.id, env).catch(() => null);
12174
- if (subsRes?.complete && rows4(data.applications).length < 1e3) {
13292
+ if (subsRes?.complete && rows5(data.applications).length < 1e3) {
12175
13293
  const subs = subsRes.objects.filter((s) => {
12176
13294
  const a = appById.get(s.metadata?.applicationId);
12177
13295
  return a?.stripeSubscriptionId === s.id && a?.stripeCustomerId === s.customer;
12178
13296
  });
12179
- const intents = new Map(rows4(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
13297
+ const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
12180
13298
  const amount = (s) => chapterBillingAmount(s, intents.get(s.metadata?.applicationId), env);
12181
13299
  const subMs = (s) => (s.created ?? 0) * 1e3;
12182
13300
  membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
@@ -12215,9 +13333,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12215
13333
  const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
12216
13334
  if (!subsRes) return json({ error: "billing lookup failed upstream" }, 502);
12217
13335
  const subById = new Map(subsRes.objects.map((s) => [s.id, s]));
12218
- const intents = new Map(rows4(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
13336
+ const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
12219
13337
  const billingRows = [];
12220
- for (const a of rows4(data.applications)) {
13338
+ for (const a of rows5(data.applications)) {
12221
13339
  if (!chapterBillingApplication(a, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
12222
13340
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
12223
13341
  const candidate = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
@@ -12253,9 +13371,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
12253
13371
  return json({
12254
13372
  billingReady: true,
12255
13373
  testMode,
12256
- truncated: !subsRes.complete || rows4(data.applications).length >= 200,
13374
+ truncated: !subsRes.complete || rows5(data.applications).length >= 200,
12257
13375
  rows: billingRows,
12258
- summary: subsRes.complete && rows4(data.applications).length < 200 ? summary : null
13376
+ summary: subsRes.complete && rows5(data.applications).length < 200 ? summary : null
12259
13377
  });
12260
13378
  };
12261
13379
 
@@ -12647,7 +13765,7 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
12647
13765
  var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
12648
13766
  function emailSpamSignals(copy) {
12649
13767
  const subject = typeof copy.subject === "string" ? copy.subject : "";
12650
- const text10 = typeof copy.text === "string" ? copy.text : "";
13768
+ const text11 = typeof copy.text === "string" ? copy.text : "";
12651
13769
  const html = typeof copy.html === "string" ? copy.html : "";
12652
13770
  const out = [];
12653
13771
  const letters = strip(subject).replace(/[^A-Za-z]/g, "");
@@ -12657,11 +13775,11 @@ function emailSpamSignals(copy) {
12657
13775
  if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
12658
13776
  const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
12659
13777
  if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
12660
- const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text10));
13778
+ const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text11));
12661
13779
  if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
12662
- if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text10))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
12663
- if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
12664
- const urls = [...`${text10}
13780
+ if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text11))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
13781
+ if (/!{2,}/.test(text11)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
13782
+ const urls = [...`${text11}
12665
13783
  ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
12666
13784
  const hosts = urls.map((url) => {
12667
13785
  try {
@@ -12674,7 +13792,7 @@ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
12674
13792
  if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
12675
13793
  if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
12676
13794
  if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
12677
- if (urls.length && words(text10).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
13795
+ if (urls.length && words(text11).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
12678
13796
  return out;
12679
13797
  }
12680
13798
  function emailSenderWarnings(input) {
@@ -12714,8 +13832,8 @@ function validateEmailReadiness(input) {
12714
13832
  ${template.text ?? ""}
12715
13833
  ${html}`;
12716
13834
  const sectionOnly = new Set((0, import_crm16.templateSectionNames)(copy));
12717
- for (const name of (0, import_crm16.templateVars)(copy)) if (name in input.groupValues && !input.groupValues[name]?.trim() && !sectionOnly.has(name)) {
12718
- problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
13835
+ for (const name2 of (0, import_crm16.templateVars)(copy)) if (name2 in input.groupValues && !input.groupValues[name2]?.trim() && !sectionOnly.has(name2)) {
13836
+ problems.push({ code: "value-missing", variable: name2, message: `Add the ${name2} used by this email.` });
12719
13837
  }
12720
13838
  }
12721
13839
  if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
@@ -12784,20 +13902,20 @@ async function updateEmailSettings(db, ctx, env, body) {
12784
13902
  if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
12785
13903
  const templates = body.emailTemplates;
12786
13904
  if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
12787
- const clean = {};
13905
+ const clean2 = {};
12788
13906
  for (const [key3, value] of Object.entries(templates)) {
12789
13907
  if (!events[key3]) continue;
12790
13908
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12791
13909
  const t = value;
12792
13910
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12793
- const subject = str2(t.subject).trim(), text10 = str2(t.text), html = str2(t.html);
13911
+ const subject = str2(t.subject).trim(), text11 = str2(t.text), html = str2(t.html);
12794
13912
  if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
12795
- const issues = (0, import_crm17.validateEmailCopy)({ subject, text: text10 }, { allowedVariables: events[key3].variables });
13913
+ const issues = (0, import_crm17.validateEmailCopy)({ subject, text: text11 }, { allowedVariables: events[key3].variables });
12796
13914
  const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
12797
13915
  if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
12798
13916
  return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
12799
13917
  }
12800
- clean[key3] = { subject, text: text10, ...html.trim() ? { html } : {}, enabled: t.enabled };
13918
+ clean2[key3] = { subject, text: text11, ...html.trim() ? { html } : {}, enabled: t.enabled };
12801
13919
  }
12802
13920
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12803
13921
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -12810,15 +13928,15 @@ async function updateEmailSettings(db, ctx, env, body) {
12810
13928
  if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
12811
13929
  if (ctx.chapter.mode === "hub") {
12812
13930
  const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
12813
- await (0, import_crm17.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
13931
+ await (0, import_crm17.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean2 }, notificationEmail, replyTo, debugEmail });
12814
13932
  } else {
12815
13933
  const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
12816
13934
  await db.transact([
12817
- { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
13935
+ { t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean2 } },
12818
13936
  { t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
12819
13937
  ]);
12820
13938
  }
12821
- const warnings = Object.fromEntries(Object.entries(clean).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
13939
+ const warnings = Object.fromEntries(Object.entries(clean2).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
12822
13940
  return json({ ok: true, warnings });
12823
13941
  }
12824
13942
 
@@ -12856,8 +13974,8 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
12856
13974
  const got = await gate5(req, env, ctx);
12857
13975
  if (got instanceof Response) return got;
12858
13976
  if (ctx.chapter.mode === "hub") {
12859
- const rows5 = await (0, import_crm18.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
12860
- return json({ sends: rows5.map((row2) => ({ ...row2, template: row2.templateId })) });
13977
+ const rows6 = await (0, import_crm18.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
13978
+ return json({ sends: rows6.map((row2) => ({ ...row2, template: row2.templateId })) });
12861
13979
  }
12862
13980
  const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
12863
13981
  const visible = [];
@@ -13087,7 +14205,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
13087
14205
  return json({
13088
14206
  targets: ctx.chapter.network.targets.map(({
13089
14207
  id: id7,
13090
- name,
14208
+ name: name2,
13091
14209
  url: targetUrl,
13092
14210
  fields,
13093
14211
  sharedNotes,
@@ -13096,7 +14214,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
13096
14214
  commercialParity
13097
14215
  }) => ({
13098
14216
  id: id7,
13099
- name,
14217
+ name: name2,
13100
14218
  url: targetUrl,
13101
14219
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
13102
14220
  sharedNotes,
@@ -13430,8 +14548,8 @@ function terms(value) {
13430
14548
  function matchesReaderIdentity(record4, search, includeEmail) {
13431
14549
  const query = terms(search);
13432
14550
  if (!query.length) return true;
13433
- const name = folded(record4.name);
13434
- const visible = [name, ...name.split(/\s+/u).filter(Boolean)];
14551
+ const name2 = folded(record4.name);
14552
+ const visible = [name2, ...name2.split(/\s+/u).filter(Boolean)];
13435
14553
  if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
13436
14554
  return query.every((term) => visible.some((candidate) => candidate.includes(term)));
13437
14555
  }
@@ -13460,14 +14578,14 @@ async function listVisibleSearchPopulation(db, where, window, count) {
13460
14578
  })));
13461
14579
  return pages.flatMap((page) => (page[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord));
13462
14580
  }
13463
- async function listReaderRecords(db, type, allowed2, emailField, window) {
14581
+ async function listReaderRecords(db, type, allowed, emailField, window) {
13464
14582
  const where = baseWhere(type, window.stage);
13465
14583
  const crmDb = db;
13466
14584
  const aggregate = await crmDb.aggregate(import_crm22.CRM_NS.record, { count: true }, { where });
13467
14585
  const candidateCount = aggregate.count ?? 0;
13468
14586
  if (window.search) {
13469
14587
  const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
13470
- const includeEmail = Boolean(emailField && allowed2.includes(emailField));
14588
+ const includeEmail = Boolean(emailField && allowed.includes(emailField));
13471
14589
  const matches2 = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
13472
14590
  return {
13473
14591
  records: matches2.slice(window.offset, window.offset + window.limit),
@@ -13560,7 +14678,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13560
14678
  const editable = reader.editableFields[input.type];
13561
14679
  if (hasFields) {
13562
14680
  const names = Object.keys(fields);
13563
- if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
14681
+ if (!editable || names.length === 0 || names.length > 32 || names.some((name2) => !editable.includes(name2))) {
13564
14682
  return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
13565
14683
  }
13566
14684
  } else if (!reader.stageTransitions.includes(input.type)) {
@@ -13625,7 +14743,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13625
14743
  });
13626
14744
  }
13627
14745
  const capabilities = await operationalReaderRecord(record4, reader, db, env, ctx);
13628
- if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
14746
+ if (hasFields && Object.keys(fields).some((name2) => !capabilities.edit?.fields.some((field) => field.id === name2))) {
13629
14747
  return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
13630
14748
  }
13631
14749
  const deps = { crm: ctx.chapter.crm, db };
@@ -13680,8 +14798,8 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13680
14798
  }
13681
14799
  }
13682
14800
  const type = url.searchParams.get("type") ?? "";
13683
- const allowed2 = reader.fields[type];
13684
- if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
14801
+ const allowed = reader.fields[type];
14802
+ if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
13685
14803
  const recordId = url.searchParams.get("recordId");
13686
14804
  if (recordId !== null) {
13687
14805
  if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
@@ -13709,7 +14827,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
13709
14827
  page = await listReaderRecords(
13710
14828
  db,
13711
14829
  type,
13712
- allowed2,
14830
+ allowed,
13713
14831
  typeDef.emailField,
13714
14832
  window
13715
14833
  );
@@ -13790,8 +14908,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
13790
14908
  async function mutateTargetRecord(db, env, ctx, target, input) {
13791
14909
  const fields = input.fields;
13792
14910
  if (fields) {
13793
- const allowed2 = target.editableFields[input.type];
13794
- if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
14911
+ const allowed = target.editableFields[input.type];
14912
+ if (!allowed || Object.keys(fields).some((name2) => !allowed.includes(name2))) {
13795
14913
  return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
13796
14914
  }
13797
14915
  } else if (!target.stageTransitions.includes(input.type)) {
@@ -14635,20 +15753,20 @@ async function networkPersonBulkSha256(value) {
14635
15753
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
14636
15754
  return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
14637
15755
  }
14638
- function assertNetworkPersonBulkRows(rows5) {
14639
- if (!Array.isArray(rows5) || rows5.length < 1) {
15756
+ function assertNetworkPersonBulkRows(rows6) {
15757
+ if (!Array.isArray(rows6) || rows6.length < 1) {
14640
15758
  throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
14641
15759
  }
14642
- if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
15760
+ if (rows6.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
14643
15761
  throw new NetworkPersonRegistryError(
14644
15762
  "bulk_rows_exceeded",
14645
15763
  `bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
14646
15764
  );
14647
15765
  }
14648
- return rows5;
15766
+ return rows6;
14649
15767
  }
14650
- async function networkPersonBulkBatchDigest(rows5) {
14651
- const bounded = assertNetworkPersonBulkRows(rows5);
15768
+ async function networkPersonBulkBatchDigest(rows6) {
15769
+ const bounded = assertNetworkPersonBulkRows(rows6);
14652
15770
  return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
14653
15771
  }
14654
15772
  async function networkPersonRegistryStateDigest(state) {
@@ -14663,7 +15781,7 @@ async function networkPersonRegistryStateDigest(state) {
14663
15781
  function rejectedValidation(errors) {
14664
15782
  return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
14665
15783
  }
14666
- async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
15784
+ async function classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state) {
14667
15785
  const aliasIds = new Set(state.aliases.map((alias) => alias.id));
14668
15786
  const batchAliases = /* @__PURE__ */ new Set();
14669
15787
  const claimOwners = /* @__PURE__ */ new Map();
@@ -14674,8 +15792,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14674
15792
  }
14675
15793
  const results = [];
14676
15794
  const values = [];
14677
- for (let index2 = 0; index2 < rows5.length; index2 += 1) {
14678
- const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
15795
+ for (let index2 = 0; index2 < rows6.length; index2 += 1) {
15796
+ const validation = validateNetworkPersonImport(rows6[index2], allowedChapterIds);
14679
15797
  if (!validation.ok || !validation.value) {
14680
15798
  const errors = validation.errors.slice(0, 8);
14681
15799
  results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
@@ -14708,8 +15826,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
14708
15826
  }
14709
15827
  return { rows: results, values };
14710
15828
  }
14711
- async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
14712
- const bounded = assertNetworkPersonBulkRows(rows5);
15829
+ async function previewNetworkPersonBulkImport(db, rows6, allowedChapterIds) {
15830
+ const bounded = assertNetworkPersonBulkRows(rows6);
14713
15831
  const state = await loadNetworkPersonRegistry(db);
14714
15832
  const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
14715
15833
  const counts = networkPersonBulkEmptyCounts();
@@ -14740,8 +15858,8 @@ function eventCounts(event2) {
14740
15858
  }
14741
15859
  async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
14742
15860
  if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
14743
- const rows5 = assertNetworkPersonBulkRows(input.rows);
14744
- const batchDigest = await networkPersonBulkBatchDigest(rows5);
15861
+ const rows6 = assertNetworkPersonBulkRows(input.rows);
15862
+ const batchDigest = await networkPersonBulkBatchDigest(rows6);
14745
15863
  if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
14746
15864
  throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
14747
15865
  }
@@ -14760,7 +15878,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14760
15878
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14761
15879
  batchDigest,
14762
15880
  stateDigest: eventString(completed, "stateDigest"),
14763
- rowCount: Number(completed.detail.rowCount) || rows5.length,
15881
+ rowCount: Number(completed.detail.rowCount) || rows6.length,
14764
15882
  counts: eventCounts(completed),
14765
15883
  rows: [],
14766
15884
  replayed: true
@@ -14768,7 +15886,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14768
15886
  }
14769
15887
  let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
14770
15888
  if (!started) {
14771
- const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
15889
+ const preview = await previewNetworkPersonBulkImport(db, rows6, allowedChapterIds);
14772
15890
  if (preview.stateDigest !== input.expectedStateDigest) {
14773
15891
  throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
14774
15892
  }
@@ -14777,7 +15895,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14777
15895
  networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
14778
15896
  batchDigest,
14779
15897
  stateDigest: input.expectedStateDigest,
14780
- rowCount: rows5.length,
15898
+ rowCount: rows6.length,
14781
15899
  previewCounts: preview.counts
14782
15900
  })
14783
15901
  ], {
@@ -14793,7 +15911,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14793
15911
  if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
14794
15912
  throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
14795
15913
  }
14796
- const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
15914
+ const classified = await classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state);
14797
15915
  const results = [];
14798
15916
  const counts = networkPersonBulkEmptyCounts();
14799
15917
  for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
@@ -14832,10 +15950,10 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14832
15950
  }
14833
15951
  if (counts.failed === 0) {
14834
15952
  await db.transact([
14835
- networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
15953
+ networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows6.length + 1, {
14836
15954
  batchDigest,
14837
15955
  stateDigest: input.expectedStateDigest,
14838
- rowCount: rows5.length,
15956
+ rowCount: rows6.length,
14839
15957
  counts
14840
15958
  })
14841
15959
  ], { mutationId: completedMutationId });
@@ -14844,7 +15962,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
14844
15962
  version: NETWORK_PERSON_BULK_IMPORT_VERSION,
14845
15963
  batchDigest,
14846
15964
  stateDigest: input.expectedStateDigest,
14847
- rowCount: rows5.length,
15965
+ rowCount: rows6.length,
14848
15966
  counts,
14849
15967
  rows: results
14850
15968
  };
@@ -14965,7 +16083,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
14965
16083
  }
14966
16084
 
14967
16085
  // src/worker-routes-network-person-registry.ts
14968
- var PATH6 = "/api/admin/network-person-registry";
16086
+ var PATH7 = "/api/admin/network-person-registry";
14969
16087
  var MAX_BODY2 = 8 * 1024 * 1024;
14970
16088
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
14971
16089
  var positiveRevision = (value) => {
@@ -14990,7 +16108,7 @@ function registryError(error) {
14990
16108
  return null;
14991
16109
  }
14992
16110
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14993
- if (url.pathname !== PATH6) return null;
16111
+ if (url.pathname !== PATH7) return null;
14994
16112
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
14995
16113
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
14996
16114
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -15002,9 +16120,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
15002
16120
  if (req.method === "POST") {
15003
16121
  let body;
15004
16122
  try {
15005
- const text10 = await req.text();
15006
- if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
15007
- const parsed = JSON.parse(text10);
16123
+ const text11 = await req.text();
16124
+ if (new TextEncoder().encode(text11).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
16125
+ const parsed = JSON.parse(text11);
15008
16126
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
15009
16127
  body = parsed;
15010
16128
  } catch {
@@ -15173,7 +16291,7 @@ var import_db13 = require("@odla-ai/db");
15173
16291
  // src/network-commercial-parity.ts
15174
16292
  var DIGEST = /^[0-9a-f]{64}$/;
15175
16293
  var MAX_RECORDS = 500;
15176
- var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
16294
+ var text9 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
15177
16295
  function parityMessage(partition, kind, value) {
15178
16296
  return [
15179
16297
  "chapter-commercial-parity",
@@ -15205,18 +16323,18 @@ async function digestWithKey(key3, partition, kind, value) {
15205
16323
  async function buildChapterCommercialParitySnapshot(input) {
15206
16324
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
15207
16325
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
15208
- const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
16326
+ const eligible = input.rows.filter((row2) => text9(row2.id) && (text9(row2.stripeCustomerId) || text9(row2.stripeSubscriptionId)));
15209
16327
  const selected2 = eligible.slice(0, maxRecords);
15210
16328
  const key3 = await parityKey(input.secret);
15211
16329
  const records = await Promise.all(selected2.map(async (row2) => {
15212
- const applicationId = text8(row2.id);
15213
- const customerId = text8(row2.stripeCustomerId);
15214
- const subscriptionId = text8(row2.stripeSubscriptionId);
16330
+ const applicationId = text9(row2.id);
16331
+ const customerId = text9(row2.stripeCustomerId);
16332
+ const subscriptionId = text9(row2.stripeSubscriptionId);
15215
16333
  return {
15216
16334
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
15217
16335
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
15218
16336
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
15219
- status: text8(row2.status, 64),
16337
+ status: text9(row2.status, 64),
15220
16338
  canceled: row2.canceled === true
15221
16339
  };
15222
16340
  }));
@@ -15242,7 +16360,7 @@ function validSnapshot(snapshot, source) {
15242
16360
  if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
15243
16361
  const seen = /* @__PURE__ */ new Set();
15244
16362
  for (const record4 of records) {
15245
- if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text8(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
16363
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text9(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
15246
16364
  const key3 = recordKey(record4);
15247
16365
  if (seen.has(key3)) return false;
15248
16366
  seen.add(key3);
@@ -15253,16 +16371,16 @@ async function reconcileCommercialParitySnapshot(input) {
15253
16371
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
15254
16372
  const sourceKey = networkCommercialSourceKey(input.source);
15255
16373
  const key3 = await parityKey(input.secret);
15256
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
15257
- const customerId = text8(row2.customerId);
15258
- const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
16374
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text9(row2.applicationId) && text9(row2.objectId)).map(async (row2) => {
16375
+ const customerId = text9(row2.customerId);
16376
+ const subscriptionId = text9(row2.subscriptionId) ?? text9(row2.objectId);
15259
16377
  return {
15260
16378
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
15261
16379
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
15262
16380
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
15263
16381
  status: null,
15264
16382
  canceled: false,
15265
- stripeStatus: text8(row2.status, 64),
16383
+ stripeStatus: text9(row2.status, 64),
15266
16384
  stripeCanceled: row2.status === "canceled"
15267
16385
  };
15268
16386
  }));
@@ -15344,14 +16462,14 @@ var PAGE2 = 500;
15344
16462
  var MAX_SCAN = 5e3;
15345
16463
  var MAX_RECORDS2 = 500;
15346
16464
  async function localCommercialRows(db) {
15347
- const rows5 = [];
16465
+ const rows6 = [];
15348
16466
  for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
15349
16467
  const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
15350
16468
  const page = Array.isArray(result.applications) ? result.applications : [];
15351
- rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
15352
- if (page.length < PAGE2) return { rows: rows5, truncated: false };
16469
+ rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
16470
+ if (page.length < PAGE2) return { rows: rows6, truncated: false };
15353
16471
  }
15354
- return { rows: rows5, truncated: true };
16472
+ return { rows: rows6, truncated: true };
15355
16473
  }
15356
16474
  var handleNetworkCommercialParity = async (req, url, env, ctx) => {
15357
16475
  if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
@@ -15486,8 +16604,8 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15486
16604
  const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
15487
16605
  return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
15488
16606
  }
15489
- const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15490
- const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
16607
+ const rows6 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
16608
+ const member = rows6.find((row2) => disputedChargeCoversMembership(charge, row2));
15491
16609
  if (!member) return { disposition: "not_applicable" };
15492
16610
  const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
15493
16611
  return { disposition: "applied", membershipId: receipt.membershipId };
@@ -15731,15 +16849,15 @@ var handleFormation = async (req, url, env, ctx) => {
15731
16849
  if (submissionId && !SUBMISSION.test(submissionId)) {
15732
16850
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
15733
16851
  }
15734
- const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
15735
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
16852
+ const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
16853
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
15736
16854
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
15737
16855
  for (const field of formation.required) {
15738
16856
  const value = body[field];
15739
16857
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
15740
16858
  }
15741
16859
  const input = {};
15742
- for (const field of allowed2) {
16860
+ for (const field of allowed) {
15743
16861
  const value = body[field];
15744
16862
  if (value === void 0) continue;
15745
16863
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -15803,10 +16921,10 @@ var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
15803
16921
  var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
15804
16922
  var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
15805
16923
  var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
15806
- function unknownKeys3(value, allowed2, path, errors) {
15807
- for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
16924
+ function unknownKeys3(value, allowed, path, errors) {
16925
+ for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
15808
16926
  }
15809
- function text9(value, path, errors, max = MAX_TEXT2, required = true) {
16927
+ function text10(value, path, errors, max = MAX_TEXT2, required = true) {
15810
16928
  if (value === void 0 && !required) return void 0;
15811
16929
  if (typeof value !== "string" || required && !value.trim()) {
15812
16930
  errors.push(`${path} must be a non-empty string`);
@@ -15832,7 +16950,7 @@ function scopeValue(value, path, errors) {
15832
16950
  return { kind: raw2.kind };
15833
16951
  }
15834
16952
  if (raw2.kind === "chapter") {
15835
- const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
16953
+ const chapterId = text10(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15836
16954
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15837
16955
  return { kind: "chapter", chapterId };
15838
16956
  }
@@ -15846,22 +16964,22 @@ function resourceValue(value, path, errors) {
15846
16964
  return null;
15847
16965
  }
15848
16966
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15849
- const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
16967
+ const id7 = text10(raw2.id, `${path}.id`, errors, 80) ?? "";
15850
16968
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15851
- const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
16969
+ const kind = text10(raw2.kind, `${path}.kind`, errors) ?? "";
15852
16970
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15853
- const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
15854
- const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
15855
- const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
16971
+ const title = text10(raw2.title, `${path}.title`, errors, 240) ?? "";
16972
+ const summary = text10(raw2.summary, `${path}.summary`, errors, 1e3, false);
16973
+ const status2 = text10(raw2.status, `${path}.status`, errors) ?? "";
15856
16974
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15857
16975
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15858
16976
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15859
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
16977
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text10(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15860
16978
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15861
16979
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15862
16980
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15863
- const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15864
- const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
16981
+ const bodyMarkdown = text10(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
16982
+ const url = text10(raw2.url, `${path}.url`, errors, 2048, false);
15865
16983
  if (url) {
15866
16984
  try {
15867
16985
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15873,9 +16991,9 @@ function resourceValue(value, path, errors) {
15873
16991
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15874
16992
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15875
16993
  const brandAsset = asset ? {
15876
- bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15877
- assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15878
- ...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
16994
+ bookId: text10(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
16995
+ assetId: text10(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
16996
+ ...asset.filename === void 0 ? {} : { filename: text10(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15879
16997
  } : void 0;
15880
16998
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15881
16999
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15890,17 +17008,17 @@ function resourceValue(value, path, errors) {
15890
17008
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15891
17009
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15892
17010
  const promotedFrom = promotedRaw ? {
15893
- chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15894
- resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
17011
+ chapterId: text10(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
17012
+ resourceId: text10(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15895
17013
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15896
17014
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15897
- promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
17015
+ promotedBy: text10(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15898
17016
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15899
17017
  } : void 0;
15900
17018
  const provenance2 = {
15901
- createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
17019
+ createdBy: text10(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15902
17020
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15903
- updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
17021
+ updatedBy: text10(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15904
17022
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15905
17023
  ...promotedFrom ? { promotedFrom } : {}
15906
17024
  };
@@ -15919,7 +17037,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15919
17037
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15920
17038
  const sourceType = sourceRaw?.type;
15921
17039
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15922
- const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
17040
+ const sourceAppId = text10(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15923
17041
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15924
17042
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15925
17043
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15927,8 +17045,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15927
17045
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15928
17046
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15929
17047
  const target = targetRaw ? {
15930
- appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15931
- environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
17048
+ appId: text10(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
17049
+ environment: text10(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15932
17050
  } : void 0;
15933
17051
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15934
17052
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -16016,17 +17134,17 @@ async function builderLibraryDigest(document) {
16016
17134
  var control2 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
16017
17135
  var local3 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
16018
17136
  async function loadActiveBuilderLibraryControls(db, targetId, environment) {
16019
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
16020
- return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
17137
+ const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
17138
+ return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
16021
17139
  }
16022
17140
  async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
16023
- const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
16024
- return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
17141
+ const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
17142
+ return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
16025
17143
  }
16026
17144
  async function loadLocalBuilderLibrary(db, appId, environment) {
16027
17145
  const id7 = `builder-library-local:${environment}`;
16028
- const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
16029
- return local3(Array.isArray(rows5) ? rows5[0] : void 0);
17146
+ const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
17147
+ return local3(Array.isArray(rows6) ? rows6[0] : void 0);
16030
17148
  }
16031
17149
  async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
16032
17150
  const { document, digest } = envelope;
@@ -17669,8 +18787,8 @@ var readable = /* @__PURE__ */ new Set([
17669
18787
  "networkCommercialEvents",
17670
18788
  "networkCommercialExceptions"
17671
18789
  ]);
17672
- var check = (ns, allowed2) => {
17673
- if (!allowed2.has(ns)) throw new TypeError("namespace outside membership authority contract");
18790
+ var check = (ns, allowed) => {
18791
+ if (!allowed.has(ns)) throw new TypeError("namespace outside membership authority contract");
17674
18792
  return physical(ns);
17675
18793
  };
17676
18794
  function createMembershipAuthorityStorage(db, secretNames) {
@@ -17696,9 +18814,9 @@ function createMembershipAuthorityStorage(db, secretNames) {
17696
18814
  const guards = opts?.guards?.map((guard) => ({ ...guard, ns: check(guard.ns, readable) }));
17697
18815
  return db.transact(mapped, { ...opts, ...guards ? { guards } : {} });
17698
18816
  },
17699
- secrets: { get(name) {
17700
- if (!secrets.has(name)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
17701
- return db.secrets.get(name);
18817
+ secrets: { get(name2) {
18818
+ if (!secrets.has(name2)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
18819
+ return db.secrets.get(name2);
17702
18820
  } }
17703
18821
  };
17704
18822
  }
@@ -17868,7 +18986,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17868
18986
  const origin = new URL(options.origin), limit = options.limit ?? 100;
17869
18987
  if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash || !Number.isSafeInteger(limit) || limit < 1 || limit > 2e3) throw new TypeError("invalid approval recovery configuration");
17870
18988
  const db = ctx.makeDb(env);
17871
- const rows5 = (await db.query({ applications: { $: {
18989
+ const rows6 = (await db.query({ applications: { $: {
17872
18990
  where: {
17873
18991
  groupId: ctx.chapter.id,
17874
18992
  stripeRuntime: env.ODLA_RUNTIME ?? "default",
@@ -17879,7 +18997,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17879
18997
  limit: limit + 1
17880
18998
  } } })).applications ?? [];
17881
18999
  let repaired = 0, pending = 0;
17882
- for (const app of rows5.slice(0, limit)) {
19000
+ for (const app of rows6.slice(0, limit)) {
17883
19001
  if (app.groupId !== ctx.chapter.id || app.stripeRuntime !== (env.ODLA_RUNTIME ?? "default") || app.status !== "approved" || app.approvalEffectsPending !== true) throw new Error("approval recovery scope mismatch");
17884
19002
  try {
17885
19003
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { approvalEffectsLastAttemptAt: Date.now() } }], {
@@ -17904,7 +19022,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
17904
19022
  pending++;
17905
19023
  }
17906
19024
  }
17907
- return { repaired, pending, more: rows5.length > limit };
19025
+ return { repaired, pending, more: rows6.length > limit };
17908
19026
  }
17909
19027
 
17910
19028
  // src/membership-chapter-delivery.ts
@@ -17920,7 +19038,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17920
19038
  };
17921
19039
  signal.addEventListener("abort", cancel, { once: true });
17922
19040
  let size = 0;
17923
- let text10 = "";
19041
+ let text11 = "";
17924
19042
  const decoder = new TextDecoder();
17925
19043
  try {
17926
19044
  while (true) {
@@ -17930,10 +19048,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17930
19048
  if (done) break;
17931
19049
  size += value.byteLength;
17932
19050
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17933
- text10 += decoder.decode(value, { stream: true });
19051
+ text11 += decoder.decode(value, { stream: true });
17934
19052
  }
17935
19053
  try {
17936
- return JSON.parse(text10 + decoder.decode());
19054
+ return JSON.parse(text11 + decoder.decode());
17937
19055
  } catch {
17938
19056
  throw new TypeError(`invalid ${label}`);
17939
19057
  }
@@ -17952,8 +19070,8 @@ var import_crm27 = require("@odla-ai/crm");
17952
19070
  function invitationDelivery(env, recipientEmail) {
17953
19071
  const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
17954
19072
  if (env.ODLA_ENV !== "dev" || !env.SEND_INVITATION_TEST) return normal;
17955
- const allowed2 = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
17956
- if (!allowed2.includes(recipientEmail.trim().toLowerCase())) return normal;
19073
+ const allowed = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
19074
+ if (!allowed.includes(recipientEmail.trim().toLowerCase())) return normal;
17957
19075
  const to = env.ODLA_INVITATION_TEST_TO?.trim();
17958
19076
  const from = env.ODLA_INVITATION_TEST_FROM?.trim();
17959
19077
  const valid = (value) => value && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(value);
@@ -18473,27 +19591,27 @@ function validatedTokens(input) {
18473
19591
  }
18474
19592
  const tokens = {};
18475
19593
  let serializedLength = 0;
18476
- for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
18477
- if (!TOKEN_NAME.test(name)) {
18478
- throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
19594
+ for (const [name2, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
19595
+ if (!TOKEN_NAME.test(name2)) {
19596
+ throw new TypeError(`signup-control theme token "${name2}" must be an allowlisted --ui-* property`);
18479
19597
  }
18480
19598
  if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
18481
- throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
19599
+ throw new TypeError(`signup-control theme token "${name2}" must be a finite string or number`);
18482
19600
  }
18483
19601
  const value = String(raw2).trim();
18484
19602
  if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
18485
- throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
19603
+ throw new TypeError(`signup-control theme token "${name2}" has an unsafe or oversized value`);
18486
19604
  }
18487
- serializedLength += name.length + value.length + 4;
19605
+ serializedLength += name2.length + value.length + 4;
18488
19606
  if (serializedLength > MAX_SERIALIZED_LENGTH) {
18489
19607
  throw new TypeError("signup-control theme exceeds the serialized size bound");
18490
19608
  }
18491
- tokens[name] = value;
19609
+ tokens[name2] = value;
18492
19610
  }
18493
19611
  return tokens;
18494
19612
  }
18495
19613
  function cssFor(tokens) {
18496
- const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
19614
+ const declarations = Object.entries(tokens).map(([name2, value]) => ` ${name2}: ${value};`);
18497
19615
  return declarations.length ? `:root {
18498
19616
  ${declarations.join("\n")}
18499
19617
  }` : "";
@@ -18608,6 +19726,7 @@ var BUILTIN_ROUTES = [
18608
19726
  handleFormation,
18609
19727
  handleMember,
18610
19728
  handleMemberContent,
19729
+ handleMemberPortal,
18611
19730
  handleGifts,
18612
19731
  handleNamedSeat,
18613
19732
  handleNamedSeatRegift,