@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.
- package/README.md +124 -0
- package/dist/{chunk-PKVHXKGJ.js → chunk-7CVP6VSO.js} +59 -1
- package/dist/chunk-7CVP6VSO.js.map +1 -0
- package/dist/{chunk-24L7FANC.js → chunk-MUUBATZJ.js} +899 -708
- package/dist/chunk-MUUBATZJ.js.map +1 -0
- package/dist/{chunk-RTJKUOFE.js → chunk-ZIYK3OV2.js} +869 -239
- package/dist/chunk-ZIYK3OV2.js.map +1 -0
- package/dist/copy-context-BJUkVUjE.d.ts +364 -0
- package/dist/index.cjs +3839 -3225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +387 -84
- package/dist/index.d.ts +387 -84
- package/dist/index.js +3842 -3228
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +672 -3
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +3 -2
- package/dist/ui/index.js +49 -3
- package/dist/ui/member/index.d.ts +254 -3
- package/dist/ui/member/index.js +48 -2
- package/dist/worker/index.cjs +1495 -376
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +221 -81
- package/dist/worker/index.d.ts +221 -81
- package/dist/worker/index.js +1493 -374
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-24L7FANC.js.map +0 -1
- package/dist/chunk-PKVHXKGJ.js.map +0 -1
- package/dist/chunk-RTJKUOFE.js.map +0 -1
- package/dist/copy-context-nsw2DwSK.d.ts +0 -899
package/dist/worker/index.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
// src/admin-second-factor.ts
|
|
2
|
+
function sessionHasSecondFactor(payload) {
|
|
3
|
+
const fva = payload?.fva;
|
|
4
|
+
return Array.isArray(fva) && typeof fva[1] === "number" && fva[1] >= 0;
|
|
5
|
+
}
|
|
6
|
+
function adminSecondFactorAllows(mode, payload, who) {
|
|
7
|
+
if (mode === "off" || sessionHasSecondFactor(payload)) return true;
|
|
8
|
+
if (mode === "report") {
|
|
9
|
+
console.warn(JSON.stringify({ event: "chapter.admin_second_factor_missing", mode, who: who ?? null }));
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
// src/worker-context.ts
|
|
2
16
|
import { initAdmin } from "@odla-ai/db";
|
|
3
17
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
@@ -27,9 +41,9 @@ function canChangeRole(ctx) {
|
|
|
27
41
|
}
|
|
28
42
|
return { ok: true };
|
|
29
43
|
}
|
|
30
|
-
async function getVaultSecret(db,
|
|
44
|
+
async function getVaultSecret(db, name2) {
|
|
31
45
|
try {
|
|
32
|
-
const value = await db.secrets.get(
|
|
46
|
+
const value = await db.secrets.get(name2);
|
|
33
47
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
34
48
|
} catch {
|
|
35
49
|
return void 0;
|
|
@@ -362,7 +376,8 @@ function createWorkerContext(options) {
|
|
|
362
376
|
return await isAdminEmail(db, u.email) ? auth.adminRole : auth.ladder[0];
|
|
363
377
|
}
|
|
364
378
|
async function isAdmin(db, u) {
|
|
365
|
-
|
|
379
|
+
const admin = await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
|
|
380
|
+
return admin && adminSecondFactorAllows(auth.adminSecondFactor ?? "off", u.payload, u.email ?? u.userId);
|
|
366
381
|
}
|
|
367
382
|
function crmSender(env) {
|
|
368
383
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return void 0;
|
|
@@ -850,14 +865,14 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
850
865
|
const prior = latest.get(key3);
|
|
851
866
|
if (!prior || order(prior, fact) <= 0) latest.set(key3, row2);
|
|
852
867
|
}
|
|
853
|
-
const
|
|
854
|
-
const customers =
|
|
855
|
-
const subscriptions =
|
|
856
|
-
const invoices =
|
|
857
|
-
const seatPayments =
|
|
858
|
-
const disputes =
|
|
859
|
-
const refunds =
|
|
860
|
-
for (const charge of
|
|
868
|
+
const rows6 = [...latest.values()];
|
|
869
|
+
const customers = rows6.filter((row2) => row2.objectKind === "customer");
|
|
870
|
+
const subscriptions = rows6.filter((row2) => row2.objectKind === "subscription");
|
|
871
|
+
const invoices = rows6.filter((row2) => row2.objectKind === "invoice");
|
|
872
|
+
const seatPayments = rows6.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
|
|
873
|
+
const disputes = rows6.filter((row2) => row2.objectKind === "dispute");
|
|
874
|
+
const refunds = rows6.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
|
|
875
|
+
for (const charge of rows6.filter((row2) => row2.eventType === "charge.refunded")) {
|
|
861
876
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
862
877
|
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
863
878
|
}
|
|
@@ -1115,11 +1130,11 @@ async function readStripeRecoveryEvent(db, eventId) {
|
|
|
1115
1130
|
}
|
|
1116
1131
|
async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
|
|
1117
1132
|
const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
|
|
1118
|
-
const
|
|
1133
|
+
const rows6 = await db.query({
|
|
1119
1134
|
networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
|
|
1120
1135
|
networkCommercialExceptions: { $: { where: { id: exceptionId2 }, limit: 1 } }
|
|
1121
1136
|
});
|
|
1122
|
-
const stored =
|
|
1137
|
+
const stored = rows6.networkCommercialEvents?.[0], exception = rows6.networkCommercialExceptions?.[0];
|
|
1123
1138
|
if (!stored) throw new Error("stored event not found");
|
|
1124
1139
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
1125
1140
|
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
@@ -1195,7 +1210,7 @@ function findGiftRef(obj) {
|
|
|
1195
1210
|
const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
|
|
1196
1211
|
const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
|
|
1197
1212
|
const candidates = [metaOf(obj), metaOf(subscription)];
|
|
1198
|
-
const pickIdentity = (
|
|
1213
|
+
const pickIdentity = (name2) => candidates.map((metadata) => typeof metadata[name2] === "string" ? String(metadata[name2]) : void 0).find(Boolean);
|
|
1199
1214
|
const appId = pickIdentity("odlaAppId");
|
|
1200
1215
|
const environment = pickIdentity("odlaEnvironment");
|
|
1201
1216
|
const dataEnvironment = pickIdentity("odlaDataEnvironment");
|
|
@@ -1292,11 +1307,11 @@ async function archivedChapterMembershipEvent(db, rawBody, app, env, ctx) {
|
|
|
1292
1307
|
if (!app.membershipRestartId) return false;
|
|
1293
1308
|
const fact = normalizeNetworkCommercialStripeEvent(JSON.parse(rawBody), "");
|
|
1294
1309
|
if (!fact.subscriptionId || fact.subscriptionId === app.stripeSubscriptionId) return false;
|
|
1295
|
-
const
|
|
1310
|
+
const rows6 = (await db.query({ membershipCheckoutHistory: { $: { where: {
|
|
1296
1311
|
applicationId: app.id,
|
|
1297
1312
|
providerSubscriptionId: fact.subscriptionId
|
|
1298
1313
|
}, limit: 2 } } })).membershipCheckoutHistory ?? [];
|
|
1299
|
-
return
|
|
1314
|
+
return rows6.length === 1 && rows6[0].appId === env.ODLA_APP_ID && rows6[0].chapterId === ctx.chapter.id && rows6[0].environment === env.ODLA_ENV && rows6[0].runtime === stripeRuntimeIdentity(env.ODLA_RUNTIME);
|
|
1300
1315
|
}
|
|
1301
1316
|
|
|
1302
1317
|
// src/crm-sync.ts
|
|
@@ -1492,10 +1507,10 @@ async function findSharedRecord(deps, record4, tag) {
|
|
|
1492
1507
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1493
1508
|
}
|
|
1494
1509
|
const nameField = def.nameField ?? "name";
|
|
1495
|
-
const
|
|
1496
|
-
if (record4.type === "company" && typeof
|
|
1510
|
+
const name2 = record4.input[nameField];
|
|
1511
|
+
if (record4.type === "company" && typeof name2 === "string" && name2.trim()) {
|
|
1497
1512
|
const found = await deps.db.query({
|
|
1498
|
-
crm_record: { $: { where: { type: record4.type, name:
|
|
1513
|
+
crm_record: { $: { where: { type: record4.type, name: name2.trim() }, limit: 1 } }
|
|
1499
1514
|
});
|
|
1500
1515
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1501
1516
|
}
|
|
@@ -1633,9 +1648,9 @@ async function backfillCrm(deps) {
|
|
|
1633
1648
|
}
|
|
1634
1649
|
|
|
1635
1650
|
// src/email-retirement.ts
|
|
1636
|
-
function isRetiredEmailCopy(subject,
|
|
1651
|
+
function isRetiredEmailCopy(subject, text11) {
|
|
1637
1652
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1638
|
-
const bodyCopy =
|
|
1653
|
+
const bodyCopy = text11.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1639
1654
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1640
1655
|
}
|
|
1641
1656
|
|
|
@@ -1660,8 +1675,8 @@ function lineHtml(line) {
|
|
|
1660
1675
|
}
|
|
1661
1676
|
return out + escapeHtml(line.slice(last));
|
|
1662
1677
|
}
|
|
1663
|
-
function textToHtml(
|
|
1664
|
-
const paragraphs =
|
|
1678
|
+
function textToHtml(text11) {
|
|
1679
|
+
const paragraphs = text11.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1665
1680
|
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1666
1681
|
return `<!doctype html>
|
|
1667
1682
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
@@ -1676,7 +1691,7 @@ function render(template, vars) {
|
|
|
1676
1691
|
}
|
|
1677
1692
|
var HTML_BODY_LIMIT = 2e5;
|
|
1678
1693
|
function renderHtmlBody(template, vars) {
|
|
1679
|
-
const escaped = Object.fromEntries(Object.entries(vars).map(([
|
|
1694
|
+
const escaped = Object.fromEntries(Object.entries(vars).map(([name2, value]) => [name2, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
|
|
1680
1695
|
return renderTemplate(template, escaped);
|
|
1681
1696
|
}
|
|
1682
1697
|
function htmlBodyIssues(html, options) {
|
|
@@ -1686,7 +1701,7 @@ function htmlBodyIssues(html, options) {
|
|
|
1686
1701
|
const unknown = new Set(templateVars(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
|
|
1687
1702
|
for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
|
|
1688
1703
|
if (options.values) {
|
|
1689
|
-
for (const
|
|
1704
|
+
for (const name2 of templateSectionNames(html)) if (typeof options.values[name2] !== "string") issues.push({ code: "value-missing", variable: name2, message: `Missing event value: ${name2} (HTML body).` });
|
|
1690
1705
|
for (const variable of templateLiveVars(html, options.values)) {
|
|
1691
1706
|
if (unknown.has(variable)) continue;
|
|
1692
1707
|
const value = options.values[variable];
|
|
@@ -1766,12 +1781,12 @@ function planDelivery(input) {
|
|
|
1766
1781
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1767
1782
|
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1768
1783
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1769
|
-
const
|
|
1784
|
+
const text11 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1770
1785
|
|
|
1771
1786
|
` + renderedText : renderedText;
|
|
1772
1787
|
const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
|
|
1773
1788
|
const htmlOut = renderedHtml && redirect ? `<p style="font:13px monospace;color:#555;margin:0 0 12px">(dev redirect; original recipient: ${escapeHtml(input.to)})</p>` + renderedHtml : renderedHtml;
|
|
1774
|
-
return { deliver: true, transport, to, subject, text:
|
|
1789
|
+
return { deliver: true, transport, to, subject, text: text11, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
|
|
1775
1790
|
}
|
|
1776
1791
|
|
|
1777
1792
|
// src/email-deliverability.ts
|
|
@@ -1807,9 +1822,9 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
1807
1822
|
function byOrder(a, b) {
|
|
1808
1823
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1809
1824
|
}
|
|
1810
|
-
function resolveTiers(group,
|
|
1811
|
-
const stored =
|
|
1812
|
-
if (
|
|
1825
|
+
function resolveTiers(group, rows6 = []) {
|
|
1826
|
+
const stored = rows6.filter((tier) => tier.active);
|
|
1827
|
+
if (rows6.length) return [...stored].sort(byOrder);
|
|
1813
1828
|
return [{
|
|
1814
1829
|
id: "standard",
|
|
1815
1830
|
groupId: String(group.id ?? ""),
|
|
@@ -1865,16 +1880,16 @@ function rowToTier(row2, groupId) {
|
|
|
1865
1880
|
}
|
|
1866
1881
|
async function loadTiers(db, group) {
|
|
1867
1882
|
const groupId = String(group.id ?? "");
|
|
1868
|
-
let
|
|
1883
|
+
let rows6;
|
|
1869
1884
|
try {
|
|
1870
|
-
|
|
1885
|
+
rows6 = (await db.query({
|
|
1871
1886
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
1872
1887
|
})).tiers;
|
|
1873
1888
|
} catch {
|
|
1874
1889
|
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
1875
1890
|
}
|
|
1876
|
-
if (!Array.isArray(
|
|
1877
|
-
const list =
|
|
1891
|
+
if (!Array.isArray(rows6)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
1892
|
+
const list = rows6.map((row2) => rowToTier(row2, groupId));
|
|
1878
1893
|
return resolveTiers(group, list);
|
|
1879
1894
|
}
|
|
1880
1895
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -1939,8 +1954,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1939
1954
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1940
1955
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1941
1956
|
function applicantVariables(app) {
|
|
1942
|
-
const
|
|
1943
|
-
return { firstName:
|
|
1957
|
+
const text11 = (v) => typeof v === "string" ? v : "";
|
|
1958
|
+
return { firstName: text11(app.firstName), lastName: text11(app.lastName), email: text11(app.email), phone: text11(app.phone), state: text11(app.state) };
|
|
1944
1959
|
}
|
|
1945
1960
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1946
1961
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -1986,8 +2001,8 @@ async function emailTierVariables(db, input) {
|
|
|
1986
2001
|
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
1987
2002
|
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
1988
2003
|
if (!tierId) return { tierName: "", tier: "" };
|
|
1989
|
-
const
|
|
1990
|
-
const tierName2 = isEmailTierName(
|
|
2004
|
+
const name2 = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
2005
|
+
const tierName2 = isEmailTierName(name2, tierId) ? name2.trim() : "";
|
|
1991
2006
|
return { tierName: tierName2, tier: tierName2, tierId };
|
|
1992
2007
|
} catch {
|
|
1993
2008
|
return { tierName: "", tier: "" };
|
|
@@ -1995,8 +2010,8 @@ async function emailTierVariables(db, input) {
|
|
|
1995
2010
|
}
|
|
1996
2011
|
|
|
1997
2012
|
// src/notify.ts
|
|
1998
|
-
function fromAddress(configured,
|
|
1999
|
-
const display =
|
|
2013
|
+
function fromAddress(configured, name2) {
|
|
2014
|
+
const display = name2?.trim();
|
|
2000
2015
|
if (!display) return configured;
|
|
2001
2016
|
const angled = configured.match(/<([^>]+)>/);
|
|
2002
2017
|
return { name: display, email: (angled ? angled[1] : configured).trim() };
|
|
@@ -2067,14 +2082,14 @@ async function sendTemplated(deps, input) {
|
|
|
2067
2082
|
if (decision.transport === "log-only") return { sent: true };
|
|
2068
2083
|
let messageId;
|
|
2069
2084
|
try {
|
|
2070
|
-
const
|
|
2071
|
-
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(
|
|
2085
|
+
const text11 = String(row2.body);
|
|
2086
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text11);
|
|
2072
2087
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
2073
2088
|
const response = await deps.sender.send({
|
|
2074
2089
|
from: fromAddress(deps.from, input.group.fromName),
|
|
2075
2090
|
to: [decision.to],
|
|
2076
2091
|
subject: String(row2.subject),
|
|
2077
|
-
text:
|
|
2092
|
+
text: text11,
|
|
2078
2093
|
...html ? { html } : {},
|
|
2079
2094
|
...headers ? { headers } : {},
|
|
2080
2095
|
replyTo: input.group.replyTo
|
|
@@ -2354,13 +2369,13 @@ async function authoritativeState(db, input) {
|
|
|
2354
2369
|
return state;
|
|
2355
2370
|
}
|
|
2356
2371
|
async function currentPolicy(db, input, restartAware, state) {
|
|
2357
|
-
const
|
|
2372
|
+
const rows6 = await db.query({
|
|
2358
2373
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
2359
2374
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
2360
2375
|
});
|
|
2361
|
-
if (!Array.isArray(
|
|
2362
|
-
const offerings =
|
|
2363
|
-
const policies =
|
|
2376
|
+
if (!Array.isArray(rows6.membershipOfferingVersion) || !Array.isArray(rows6.discountPolicyVersion) || rows6.membershipOfferingVersion.length >= 200 || rows6.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
2377
|
+
const offerings = rows6.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
2378
|
+
const policies = rows6.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
|
|
2364
2379
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
2365
2380
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
2366
2381
|
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
@@ -2478,13 +2493,13 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
2478
2493
|
// src/checkout-named-seat.ts
|
|
2479
2494
|
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
2480
2495
|
const row2 = value;
|
|
2481
|
-
const
|
|
2496
|
+
const name2 = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
2482
2497
|
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
2483
2498
|
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
2484
|
-
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) ||
|
|
2499
|
+
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
|
|
2485
2500
|
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
2486
2501
|
}
|
|
2487
|
-
return { recipientName:
|
|
2502
|
+
return { recipientName: name2, recipientEmail: email };
|
|
2488
2503
|
}
|
|
2489
2504
|
async function checkoutNamedSeatIdentity(target, email) {
|
|
2490
2505
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -2925,14 +2940,14 @@ async function readMembershipAccess(db, input, now) {
|
|
|
2925
2940
|
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2926
2941
|
requireMembershipScope(entitlement, input);
|
|
2927
2942
|
const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
2928
|
-
const
|
|
2943
|
+
const allowed = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
|
|
2929
2944
|
return {
|
|
2930
2945
|
membershipId: id7,
|
|
2931
|
-
allowed
|
|
2946
|
+
allowed,
|
|
2932
2947
|
status: String(member.status),
|
|
2933
2948
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
2934
2949
|
revision: Number(member.revision),
|
|
2935
|
-
restartEligible: !
|
|
2950
|
+
restartEligible: !allowed && membershipCanRestart(member, now)
|
|
2936
2951
|
};
|
|
2937
2952
|
}
|
|
2938
2953
|
async function verifyMembershipAccessReceipt(value, input, now) {
|
|
@@ -3077,11 +3092,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
|
3077
3092
|
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
3078
3093
|
throw new Error("Stripe returned an invalid commercial list response");
|
|
3079
3094
|
}
|
|
3080
|
-
const
|
|
3081
|
-
if (
|
|
3082
|
-
objects.push(...
|
|
3095
|
+
const rows6 = page.data.map(asRaw).filter((row2) => row2 !== null);
|
|
3096
|
+
if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
3097
|
+
objects.push(...rows6);
|
|
3083
3098
|
if (!page.has_more) return { objects, complete: true };
|
|
3084
|
-
const cursor =
|
|
3099
|
+
const cursor = rows6.at(-1)?.id;
|
|
3085
3100
|
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
3086
3101
|
startingAfter = cursor;
|
|
3087
3102
|
}
|
|
@@ -3214,7 +3229,7 @@ async function approveApplication(input) {
|
|
|
3214
3229
|
try {
|
|
3215
3230
|
if (!duplicate) {
|
|
3216
3231
|
const write = await db.transact(
|
|
3217
|
-
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3232
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvedAt: Date.now(), approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3218
3233
|
{ mutationId: `approve:${id7}`, guards: [{
|
|
3219
3234
|
ns: "applications",
|
|
3220
3235
|
id: id7,
|
|
@@ -3497,8 +3512,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
3497
3512
|
throw new Error("gift issuance conflict");
|
|
3498
3513
|
}
|
|
3499
3514
|
}
|
|
3500
|
-
function buyerGiftClaims(
|
|
3501
|
-
return
|
|
3515
|
+
function buyerGiftClaims(rows6, now) {
|
|
3516
|
+
return rows6.map((row2) => {
|
|
3502
3517
|
const stored = String(row2.status ?? "pending");
|
|
3503
3518
|
const status2 = stored === "pending" && Number(row2.codeExpiresAt) <= now ? "expired" : stored;
|
|
3504
3519
|
return {
|
|
@@ -3644,8 +3659,8 @@ async function revokeGiftClaim(db, claim, reason, now) {
|
|
|
3644
3659
|
// src/payments-route-support.ts
|
|
3645
3660
|
async function findPaymentApplication(db, event2) {
|
|
3646
3661
|
if ("applicationId" in event2 && event2.applicationId) {
|
|
3647
|
-
const
|
|
3648
|
-
return
|
|
3662
|
+
const rows6 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
|
|
3663
|
+
return rows6.applications?.[0];
|
|
3649
3664
|
}
|
|
3650
3665
|
if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
|
|
3651
3666
|
const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
|
|
@@ -3836,6 +3851,306 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
|
3836
3851
|
return membershipFail("membership_dispute_retry_required");
|
|
3837
3852
|
}
|
|
3838
3853
|
|
|
3854
|
+
// src/member-system-authority.ts
|
|
3855
|
+
var rows2 = (group) => group.fields.map((id7) => ({ ...group, id: id7 }));
|
|
3856
|
+
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
3857
|
+
...rows2({
|
|
3858
|
+
scope: "network",
|
|
3859
|
+
writer: "network_person",
|
|
3860
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3861
|
+
sensitivity: "pii",
|
|
3862
|
+
memberAccess: "edit",
|
|
3863
|
+
fields: [
|
|
3864
|
+
"person.legalName",
|
|
3865
|
+
"person.preferredName",
|
|
3866
|
+
"person.namePronunciation",
|
|
3867
|
+
"person.pronouns",
|
|
3868
|
+
"person.displayLegalName",
|
|
3869
|
+
"person.primaryEmail",
|
|
3870
|
+
"person.secondaryEmails",
|
|
3871
|
+
"person.phone",
|
|
3872
|
+
"person.linkedinUrl",
|
|
3873
|
+
"person.photo",
|
|
3874
|
+
"person.homeAddress",
|
|
3875
|
+
"person.fulfillmentFit",
|
|
3876
|
+
"person.fulfillmentSize"
|
|
3877
|
+
]
|
|
3878
|
+
}),
|
|
3879
|
+
...rows2({
|
|
3880
|
+
scope: "network",
|
|
3881
|
+
writer: "network_person",
|
|
3882
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3883
|
+
sensitivity: "ordinary",
|
|
3884
|
+
memberAccess: "edit",
|
|
3885
|
+
fields: [
|
|
3886
|
+
"person.title",
|
|
3887
|
+
"person.company",
|
|
3888
|
+
"person.companyObservedAt",
|
|
3889
|
+
"person.region",
|
|
3890
|
+
"person.whyText",
|
|
3891
|
+
"person.whyTokens",
|
|
3892
|
+
"person.skillsOffered",
|
|
3893
|
+
"person.openToRequests",
|
|
3894
|
+
"person.directoryPhotoConsent",
|
|
3895
|
+
"person.profilePrompts",
|
|
3896
|
+
"person.introducedByText"
|
|
3897
|
+
]
|
|
3898
|
+
}),
|
|
3899
|
+
...rows2({
|
|
3900
|
+
scope: "network",
|
|
3901
|
+
writer: "network_person",
|
|
3902
|
+
projections: ["bnf_admin", "chapter_admin"],
|
|
3903
|
+
sensitivity: "restricted",
|
|
3904
|
+
memberAccess: "none",
|
|
3905
|
+
fields: [
|
|
3906
|
+
"person.linkedinMemberId",
|
|
3907
|
+
"person.identityClaims",
|
|
3908
|
+
"person.aliases",
|
|
3909
|
+
"person.mergeHistory",
|
|
3910
|
+
"person.introducedByPersonId",
|
|
3911
|
+
"person.introducedAt",
|
|
3912
|
+
"person.firstTouch",
|
|
3913
|
+
"person.globalSuppression",
|
|
3914
|
+
"person.deceasedAt",
|
|
3915
|
+
"person.archivedAt"
|
|
3916
|
+
]
|
|
3917
|
+
}),
|
|
3918
|
+
...rows2({
|
|
3919
|
+
scope: "network",
|
|
3920
|
+
writer: "network_person",
|
|
3921
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3922
|
+
sensitivity: "sensitive",
|
|
3923
|
+
memberAccess: "edit",
|
|
3924
|
+
fields: ["person.accreditationSelfCertification", "person.accreditationCategory", "person.accreditationExpiresAt"]
|
|
3925
|
+
}),
|
|
3926
|
+
...rows2({
|
|
3927
|
+
scope: "network",
|
|
3928
|
+
writer: "network_person",
|
|
3929
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3930
|
+
sensitivity: "restricted",
|
|
3931
|
+
memberAccess: "read",
|
|
3932
|
+
fields: ["person.acknowledgements"]
|
|
3933
|
+
}),
|
|
3934
|
+
...rows2({
|
|
3935
|
+
scope: "provider",
|
|
3936
|
+
writer: "clerk",
|
|
3937
|
+
projections: ["clerk_identity", "bnf_admin", "chapter_admin"],
|
|
3938
|
+
sensitivity: "restricted",
|
|
3939
|
+
memberAccess: "none",
|
|
3940
|
+
fields: ["identity.clerkUserIds", "identity.clerkEmailVerification"]
|
|
3941
|
+
}),
|
|
3942
|
+
...rows2({
|
|
3943
|
+
scope: "chapter",
|
|
3944
|
+
writer: "chapter_journey",
|
|
3945
|
+
projections: ["chapter_admin", "chapter_crm", "bnf_admin"],
|
|
3946
|
+
sensitivity: "ordinary",
|
|
3947
|
+
memberAccess: "none",
|
|
3948
|
+
fields: [
|
|
3949
|
+
"journey.chapterId",
|
|
3950
|
+
"journey.stage",
|
|
3951
|
+
"journey.stageEnteredAt",
|
|
3952
|
+
"journey.stageHistory",
|
|
3953
|
+
"journey.ending",
|
|
3954
|
+
"journey.endingReason",
|
|
3955
|
+
"journey.source",
|
|
3956
|
+
"journey.entryChapterId",
|
|
3957
|
+
"journey.route",
|
|
3958
|
+
"journey.flags",
|
|
3959
|
+
"journey.applicationStartedAt",
|
|
3960
|
+
"journey.appliedAt",
|
|
3961
|
+
"journey.paidAt",
|
|
3962
|
+
"journey.callBookedAt",
|
|
3963
|
+
"journey.metAt",
|
|
3964
|
+
"journey.approvedAt",
|
|
3965
|
+
"journey.onboardedAt",
|
|
3966
|
+
"journey.firstLoginAt",
|
|
3967
|
+
"journey.firstDealAt"
|
|
3968
|
+
]
|
|
3969
|
+
}),
|
|
3970
|
+
...rows2({
|
|
3971
|
+
scope: "chapter",
|
|
3972
|
+
writer: "chapter_journey",
|
|
3973
|
+
projections: ["chapter_admin", "chapter_crm"],
|
|
3974
|
+
sensitivity: "sensitive",
|
|
3975
|
+
memberAccess: "none",
|
|
3976
|
+
fields: [
|
|
3977
|
+
"journey.relationshipOwnerId",
|
|
3978
|
+
"journey.lastContactAt",
|
|
3979
|
+
"journey.nextAction",
|
|
3980
|
+
"journey.owes",
|
|
3981
|
+
"journey.asks",
|
|
3982
|
+
"journey.preparation",
|
|
3983
|
+
"journey.notes"
|
|
3984
|
+
]
|
|
3985
|
+
}),
|
|
3986
|
+
...rows2({
|
|
3987
|
+
scope: "chapter",
|
|
3988
|
+
writer: "chapter_journey",
|
|
3989
|
+
projections: ["chapter_admin", "chapter_crm", "member_self_service"],
|
|
3990
|
+
sensitivity: "ordinary",
|
|
3991
|
+
memberAccess: "edit",
|
|
3992
|
+
fields: ["journey.dealFlowConsent", "journey.directoryConsent", "journey.emailConsent", "journey.smsConsent"]
|
|
3993
|
+
}),
|
|
3994
|
+
...rows2({
|
|
3995
|
+
scope: "chapter",
|
|
3996
|
+
writer: "chapter_journey",
|
|
3997
|
+
projections: ["chapter_admin", "bnf_admin", "member_self_service"],
|
|
3998
|
+
sensitivity: "ordinary",
|
|
3999
|
+
memberAccess: "read",
|
|
4000
|
+
fields: ["journey.memberNumber"]
|
|
4001
|
+
}),
|
|
4002
|
+
...rows2({
|
|
4003
|
+
scope: "network",
|
|
4004
|
+
writer: "network_membership",
|
|
4005
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4006
|
+
sensitivity: "ordinary",
|
|
4007
|
+
memberAccess: "read",
|
|
4008
|
+
fields: [
|
|
4009
|
+
"membership.chapterId",
|
|
4010
|
+
"membership.journeyId",
|
|
4011
|
+
"membership.termStartsAt",
|
|
4012
|
+
"membership.termEndsAt",
|
|
4013
|
+
"membership.tier",
|
|
4014
|
+
"membership.status",
|
|
4015
|
+
"membership.joinedAt",
|
|
4016
|
+
"membership.autoRenews",
|
|
4017
|
+
"membership.renewalAt",
|
|
4018
|
+
"membership.cancelsAt",
|
|
4019
|
+
"membership.graceEndsAt",
|
|
4020
|
+
"membership.foundingCohort",
|
|
4021
|
+
"membership.offeringPolicyVersion",
|
|
4022
|
+
"membership.discountPolicyVersion"
|
|
4023
|
+
]
|
|
4024
|
+
}),
|
|
4025
|
+
...rows2({
|
|
4026
|
+
scope: "network",
|
|
4027
|
+
writer: "network_commercial_projection",
|
|
4028
|
+
projections: ["bnf_admin", "chapter_admin"],
|
|
4029
|
+
sensitivity: "restricted",
|
|
4030
|
+
memberAccess: "none",
|
|
4031
|
+
fields: [
|
|
4032
|
+
"membership.stripeCustomerId",
|
|
4033
|
+
"membership.stripeSubscriptionId",
|
|
4034
|
+
"membership.stripePriceId",
|
|
4035
|
+
"membership.lastInvoiceId",
|
|
4036
|
+
"membership.paymentOutcome",
|
|
4037
|
+
"membership.refundOutcome"
|
|
4038
|
+
]
|
|
4039
|
+
}),
|
|
4040
|
+
...rows2({
|
|
4041
|
+
scope: "network",
|
|
4042
|
+
writer: "network_discount_policy",
|
|
4043
|
+
projections: ["bnf_admin", "chapter_admin", "stripe_billing"],
|
|
4044
|
+
sensitivity: "ordinary",
|
|
4045
|
+
memberAccess: "none",
|
|
4046
|
+
fields: [
|
|
4047
|
+
"discountPolicy.chapterId",
|
|
4048
|
+
"discountPolicy.version",
|
|
4049
|
+
"discountPolicy.eligibleOfferingIds",
|
|
4050
|
+
"discountPolicy.percentOff",
|
|
4051
|
+
"discountPolicy.amountOff",
|
|
4052
|
+
"discountPolicy.activatesOn",
|
|
4053
|
+
"discountPolicy.invoiceCount",
|
|
4054
|
+
"discountPolicy.expiresAt",
|
|
4055
|
+
"discountPolicy.stackingOrder",
|
|
4056
|
+
"discountPolicy.status"
|
|
4057
|
+
]
|
|
4058
|
+
}),
|
|
4059
|
+
...rows2({
|
|
4060
|
+
scope: "network",
|
|
4061
|
+
writer: "network_entitlement",
|
|
4062
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service", "stripe_billing"],
|
|
4063
|
+
sensitivity: "ordinary",
|
|
4064
|
+
memberAccess: "read",
|
|
4065
|
+
fields: [
|
|
4066
|
+
"entitlement.membershipId",
|
|
4067
|
+
"entitlement.kind",
|
|
4068
|
+
"entitlement.priceCents",
|
|
4069
|
+
"entitlement.currency",
|
|
4070
|
+
"entitlement.payerPersonId",
|
|
4071
|
+
"entitlement.purchasedAt",
|
|
4072
|
+
"entitlement.termEndsAt",
|
|
4073
|
+
"entitlement.renewsWithMembership",
|
|
4074
|
+
"entitlement.status"
|
|
4075
|
+
]
|
|
4076
|
+
}),
|
|
4077
|
+
...rows2({
|
|
4078
|
+
scope: "network",
|
|
4079
|
+
writer: "network_seat",
|
|
4080
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4081
|
+
sensitivity: "pii",
|
|
4082
|
+
memberAccess: "read",
|
|
4083
|
+
fields: [
|
|
4084
|
+
"seat.membershipId",
|
|
4085
|
+
"seat.entitlementId",
|
|
4086
|
+
"seat.personId",
|
|
4087
|
+
"seat.recipientName",
|
|
4088
|
+
"seat.recipientEmail",
|
|
4089
|
+
"seat.role",
|
|
4090
|
+
"seat.status",
|
|
4091
|
+
"seat.acceptanceExpiresAt",
|
|
4092
|
+
"seat.acceptedAt",
|
|
4093
|
+
"seat.activatedAt",
|
|
4094
|
+
"seat.refundedAt"
|
|
4095
|
+
]
|
|
4096
|
+
}),
|
|
4097
|
+
...rows2({
|
|
4098
|
+
scope: "network",
|
|
4099
|
+
writer: "network_referral_ledger",
|
|
4100
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4101
|
+
sensitivity: "ordinary",
|
|
4102
|
+
memberAccess: "read",
|
|
4103
|
+
fields: [
|
|
4104
|
+
"referral.referrerPersonId",
|
|
4105
|
+
"referral.referredPersonId",
|
|
4106
|
+
"referral.qualifiedMembershipId",
|
|
4107
|
+
"referral.creditCents",
|
|
4108
|
+
"referral.status",
|
|
4109
|
+
"referral.appliedInvoiceId",
|
|
4110
|
+
"referral.carryForwardCents",
|
|
4111
|
+
"referral.adjustmentOf"
|
|
4112
|
+
]
|
|
4113
|
+
}),
|
|
4114
|
+
...rows2({
|
|
4115
|
+
scope: "network",
|
|
4116
|
+
writer: "network_investment_ledger",
|
|
4117
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4118
|
+
sensitivity: "restricted",
|
|
4119
|
+
memberAccess: "read",
|
|
4120
|
+
fields: [
|
|
4121
|
+
"investment.personId",
|
|
4122
|
+
"investment.chapterId",
|
|
4123
|
+
"investment.season",
|
|
4124
|
+
"investment.purpose",
|
|
4125
|
+
"investment.targetCount",
|
|
4126
|
+
"investment.status",
|
|
4127
|
+
"investment.amountCents",
|
|
4128
|
+
"investment.closedAt",
|
|
4129
|
+
"investment.sourceReference"
|
|
4130
|
+
]
|
|
4131
|
+
}),
|
|
4132
|
+
...rows2({
|
|
4133
|
+
scope: "provider",
|
|
4134
|
+
writer: "stripe",
|
|
4135
|
+
projections: ["stripe_billing", "bnf_admin", "chapter_admin"],
|
|
4136
|
+
sensitivity: "restricted",
|
|
4137
|
+
memberAccess: "none",
|
|
4138
|
+
fields: [
|
|
4139
|
+
"commercial.charge",
|
|
4140
|
+
"commercial.invoice",
|
|
4141
|
+
"commercial.refund",
|
|
4142
|
+
"commercial.providerSubscription",
|
|
4143
|
+
"commercial.paymentMethod"
|
|
4144
|
+
]
|
|
4145
|
+
})
|
|
4146
|
+
];
|
|
4147
|
+
var MEMBER_SELF_EDITABLE_FIELDS = new Set(
|
|
4148
|
+
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess === "edit").map((row2) => row2.id)
|
|
4149
|
+
);
|
|
4150
|
+
var MEMBER_SELF_READABLE_FIELDS = new Set(
|
|
4151
|
+
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
|
|
4152
|
+
);
|
|
4153
|
+
|
|
3839
4154
|
// src/member-system-contract.ts
|
|
3840
4155
|
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3841
4156
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -3847,8 +4162,8 @@ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink)
|
|
|
3847
4162
|
|
|
3848
4163
|
// src/stripe-webhook-partition.ts
|
|
3849
4164
|
var firstRow = async (db, ns, q) => {
|
|
3850
|
-
const
|
|
3851
|
-
return Array.isArray(
|
|
4165
|
+
const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4166
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3852
4167
|
};
|
|
3853
4168
|
var stripeEventIdentity = (event2) => ({
|
|
3854
4169
|
...event2.appId ? { appId: event2.appId } : {},
|
|
@@ -3955,8 +4270,8 @@ async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest,
|
|
|
3955
4270
|
// src/stripe-event-reconcile.ts
|
|
3956
4271
|
async function firstRow2(db, ns, q) {
|
|
3957
4272
|
const res = await db.query({ [ns]: { $: q } });
|
|
3958
|
-
const
|
|
3959
|
-
return Array.isArray(
|
|
4273
|
+
const rows6 = res[ns];
|
|
4274
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3960
4275
|
}
|
|
3961
4276
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3962
4277
|
const db = ctx.makeDb(env);
|
|
@@ -4273,19 +4588,19 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
4273
4588
|
const db = raw2, target = stripeEventTarget(env, ctx);
|
|
4274
4589
|
const local4 = (row3) => row3.chapterId === target.chapterId && row3.appId === target.appId && row3.environment === target.environment && row3.runtime === target.runtime;
|
|
4275
4590
|
if (req.method === "GET") {
|
|
4276
|
-
const
|
|
4591
|
+
const rows6 = (await db.query({ stripeEventReceipts: { $: {
|
|
4277
4592
|
where: { chapterId: target.chapterId, runtime: target.runtime },
|
|
4278
4593
|
order: { receivedAt: "desc" },
|
|
4279
4594
|
limit: 500
|
|
4280
4595
|
} } })).stripeEventReceipts ?? [];
|
|
4281
|
-
const pending =
|
|
4596
|
+
const pending = rows6.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
|
|
4282
4597
|
return json({ rows: pending.map((row3) => ({
|
|
4283
4598
|
eventId: row3.eventId,
|
|
4284
4599
|
kind: row3.kind,
|
|
4285
4600
|
disposition: row3.disposition,
|
|
4286
4601
|
receivedAt: row3.receivedAt,
|
|
4287
4602
|
retryReady: row3.retryReady === true
|
|
4288
|
-
})), truncated:
|
|
4603
|
+
})), truncated: rows6.length >= 500 });
|
|
4289
4604
|
}
|
|
4290
4605
|
let eventId;
|
|
4291
4606
|
try {
|
|
@@ -4471,9 +4786,9 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
4471
4786
|
var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
|
|
4472
4787
|
async function memberSessionApplication(db, chapterId, runtime, identity2) {
|
|
4473
4788
|
const lookup = async (where) => {
|
|
4474
|
-
const
|
|
4475
|
-
if (!Array.isArray(
|
|
4476
|
-
return
|
|
4789
|
+
const rows6 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
|
|
4790
|
+
if (!Array.isArray(rows6) || rows6.length >= 100) throw new Error("member identity lookup incomplete");
|
|
4791
|
+
return rows6.filter((row2) => local(row2, chapterId, runtime));
|
|
4477
4792
|
};
|
|
4478
4793
|
let apps = await lookup({ clerkUserId: identity2.userId });
|
|
4479
4794
|
let emailFallback = false;
|
|
@@ -4850,10 +5165,10 @@ async function namedSeatRecipientClaimId(input, email) {
|
|
|
4850
5165
|
}
|
|
4851
5166
|
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4852
5167
|
membershipClock(now);
|
|
4853
|
-
const
|
|
5168
|
+
const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4854
5169
|
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4855
5170
|
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4856
|
-
if (
|
|
5171
|
+
if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
4857
5172
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4858
5173
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4859
5174
|
requireMembershipScope(member, input);
|
|
@@ -4870,7 +5185,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4870
5185
|
id: id7,
|
|
4871
5186
|
...membershipScope(input),
|
|
4872
5187
|
primaryMembershipId: memberId,
|
|
4873
|
-
recipientName:
|
|
5188
|
+
recipientName: name2,
|
|
4874
5189
|
recipientEmailNormalized: email,
|
|
4875
5190
|
purchaserUserId: input.purchaserUserId,
|
|
4876
5191
|
recipientClaimId: claimId,
|
|
@@ -4917,7 +5232,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4917
5232
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4918
5233
|
}
|
|
4919
5234
|
requireMembershipScope(seat, input);
|
|
4920
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !==
|
|
5235
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name2 || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4921
5236
|
return seat;
|
|
4922
5237
|
}
|
|
4923
5238
|
|
|
@@ -5108,9 +5423,9 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
5108
5423
|
membershipClock(now);
|
|
5109
5424
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
5110
5425
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
5111
|
-
const
|
|
5426
|
+
const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5112
5427
|
let lifted = 0;
|
|
5113
|
-
for (const member of
|
|
5428
|
+
for (const member of rows6.slice(0, limit)) {
|
|
5114
5429
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
5115
5430
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
5116
5431
|
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
@@ -5126,7 +5441,7 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
5126
5441
|
] });
|
|
5127
5442
|
lifted++;
|
|
5128
5443
|
}
|
|
5129
|
-
return { lifted, more:
|
|
5444
|
+
return { lifted, more: rows6.length > limit };
|
|
5130
5445
|
}
|
|
5131
5446
|
|
|
5132
5447
|
// src/membership-seat-reprice.ts
|
|
@@ -5139,9 +5454,9 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
5139
5454
|
const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
|
|
5140
5455
|
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
5141
5456
|
const target = current.seatPriceCents;
|
|
5142
|
-
const
|
|
5143
|
-
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more:
|
|
5144
|
-
for (const member of
|
|
5457
|
+
const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5458
|
+
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows6.length > limit };
|
|
5459
|
+
for (const member of rows6.slice(0, limit)) {
|
|
5145
5460
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
5146
5461
|
const id7 = String(member.id), skip = (reason) => {
|
|
5147
5462
|
receipt.skipped.push({ membershipId: id7, reason });
|
|
@@ -5240,8 +5555,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
5240
5555
|
}
|
|
5241
5556
|
}
|
|
5242
5557
|
if (!one) {
|
|
5243
|
-
const
|
|
5244
|
-
return json({ seats:
|
|
5558
|
+
const rows6 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
5559
|
+
return json({ seats: rows6.slice(0, 200).map(project), truncated: rows6.length > 200 });
|
|
5245
5560
|
}
|
|
5246
5561
|
const seat = await membershipLoad(db, "networkSeat", one[1]);
|
|
5247
5562
|
if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
|
|
@@ -5460,8 +5775,8 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
|
5460
5775
|
const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
|
|
5461
5776
|
const found = await db.query({ networkMembership: page, networkSeat: page });
|
|
5462
5777
|
const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
|
|
5463
|
-
const
|
|
5464
|
-
const disputes =
|
|
5778
|
+
const rows6 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
|
|
5779
|
+
const disputes = rows6.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5465
5780
|
return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
|
|
5466
5781
|
}
|
|
5467
5782
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
@@ -5511,9 +5826,9 @@ function stateOf(item, all, now, window, rivals) {
|
|
|
5511
5826
|
var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
|
|
5512
5827
|
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
5513
5828
|
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
5514
|
-
function normalize(
|
|
5829
|
+
function normalize(rows6, collection, parse, invalid2) {
|
|
5515
5830
|
const out = [];
|
|
5516
|
-
for (const row2 of
|
|
5831
|
+
for (const row2 of rows6) {
|
|
5517
5832
|
try {
|
|
5518
5833
|
out.push({ value: parse(row2.snapshot), row: row2 });
|
|
5519
5834
|
} catch (error) {
|
|
@@ -5524,11 +5839,11 @@ function normalize(rows5, collection, parse, invalid2) {
|
|
|
5524
5839
|
}
|
|
5525
5840
|
async function listMembershipPolicyVersions(db, input) {
|
|
5526
5841
|
const where = input.chapterId ? { chapterId: input.chapterId } : {};
|
|
5527
|
-
const
|
|
5842
|
+
const rows6 = await db.query({
|
|
5528
5843
|
membershipOfferingVersion: { $: { where, limit: LIMIT } },
|
|
5529
5844
|
discountPolicyVersion: { $: { where, limit: LIMIT } }
|
|
5530
5845
|
});
|
|
5531
|
-
const offeringRows =
|
|
5846
|
+
const offeringRows = rows6.membershipOfferingVersion ?? [], policyRows = rows6.discountPolicyVersion ?? [];
|
|
5532
5847
|
const invalid2 = [];
|
|
5533
5848
|
const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
|
|
5534
5849
|
const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
|
|
@@ -5668,22 +5983,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
5668
5983
|
const skip = (membershipId, reason) => {
|
|
5669
5984
|
receipt.skipped.push({ membershipId, reason });
|
|
5670
5985
|
};
|
|
5671
|
-
let
|
|
5986
|
+
let rows6;
|
|
5672
5987
|
if (input.membershipIds) {
|
|
5673
|
-
|
|
5988
|
+
rows6 = [];
|
|
5674
5989
|
for (const id7 of input.membershipIds) {
|
|
5675
5990
|
const member = await membershipLoad(db, "networkMembership", id7);
|
|
5676
5991
|
if (!member) skip(id7, "not_found");
|
|
5677
5992
|
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
5678
|
-
else
|
|
5993
|
+
else rows6.push(member);
|
|
5679
5994
|
}
|
|
5680
5995
|
} else {
|
|
5681
5996
|
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5682
5997
|
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
5683
5998
|
receipt.more = found.length > limit;
|
|
5684
|
-
|
|
5999
|
+
rows6 = found.slice(0, limit);
|
|
5685
6000
|
}
|
|
5686
|
-
for (const member of
|
|
6001
|
+
for (const member of rows6) {
|
|
5687
6002
|
const id7 = String(member.id);
|
|
5688
6003
|
if (member.discountPolicyVersionId === policy2.id) {
|
|
5689
6004
|
receipt.unchanged.push(id7);
|
|
@@ -5979,13 +6294,13 @@ var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
|
5979
6294
|
function resolveApplication(a) {
|
|
5980
6295
|
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
5981
6296
|
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
5982
|
-
for (const [
|
|
6297
|
+
for (const [name2, arr] of [["required", required], ["optional", optional]]) {
|
|
5983
6298
|
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
5984
|
-
throw new Error(`defineChapter.application.${
|
|
6299
|
+
throw new Error(`defineChapter.application.${name2}: must be an array of field-name strings`);
|
|
5985
6300
|
}
|
|
5986
6301
|
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
5987
6302
|
if (unsafe) {
|
|
5988
|
-
throw new Error(`defineChapter.application.${
|
|
6303
|
+
throw new Error(`defineChapter.application.${name2}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
5989
6304
|
}
|
|
5990
6305
|
}
|
|
5991
6306
|
const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
@@ -6037,14 +6352,14 @@ function hasDisclaimerAck(fields) {
|
|
|
6037
6352
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
6038
6353
|
function applicantProfile(chapter, fields) {
|
|
6039
6354
|
const app = chapter.application;
|
|
6040
|
-
const
|
|
6355
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
6041
6356
|
const profile = {};
|
|
6042
6357
|
for (const f of [...app.required, ...app.optional]) {
|
|
6043
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
6358
|
+
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
6044
6359
|
const v = fields[f];
|
|
6045
6360
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
6046
6361
|
}
|
|
6047
|
-
if (fields.focus !== void 0 &&
|
|
6362
|
+
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
6048
6363
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
6049
6364
|
}
|
|
6050
6365
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -6163,10 +6478,10 @@ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
|
|
|
6163
6478
|
applications: { $: { where: { emailNormalized: email }, limit: 10 } },
|
|
6164
6479
|
$users: { $: { where: { email }, limit: 2 } }
|
|
6165
6480
|
});
|
|
6166
|
-
const
|
|
6481
|
+
const rows6 = data.applications ?? [], others = rows6.filter((row2) => row2.id !== applicationId);
|
|
6167
6482
|
const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
|
|
6168
6483
|
if (others.length !== closed.length) return conflict();
|
|
6169
|
-
const original =
|
|
6484
|
+
const original = rows6.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
|
|
6170
6485
|
const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
|
|
6171
6486
|
if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
|
|
6172
6487
|
const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
|
|
@@ -6290,15 +6605,15 @@ async function signupControlDigest(document) {
|
|
|
6290
6605
|
|
|
6291
6606
|
// src/admission-store-read.ts
|
|
6292
6607
|
async function readAdmissionRows(db, namespace, where, maximum) {
|
|
6293
|
-
const
|
|
6608
|
+
const rows6 = [];
|
|
6294
6609
|
for (let offset = 0; offset <= maximum; offset += 500) {
|
|
6295
6610
|
const result = await db.query({ [namespace]: { $: { where, order: { id: "asc" }, limit: 500, offset } } });
|
|
6296
6611
|
const page = Array.isArray(result[namespace]) ? result[namespace] : [];
|
|
6297
|
-
|
|
6298
|
-
if (
|
|
6612
|
+
rows6.push(...page);
|
|
6613
|
+
if (rows6.length > maximum) throw new Error("admission_assignment_limit");
|
|
6299
6614
|
if (page.length < 500) break;
|
|
6300
6615
|
}
|
|
6301
|
-
return
|
|
6616
|
+
return rows6;
|
|
6302
6617
|
}
|
|
6303
6618
|
|
|
6304
6619
|
// src/admission-grants.ts
|
|
@@ -6381,7 +6696,7 @@ async function createAdmissionGrantEnvelope(document) {
|
|
|
6381
6696
|
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
6382
6697
|
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
6383
6698
|
}
|
|
6384
|
-
var
|
|
6699
|
+
var rows3 = (value) => Array.isArray(value) ? value : [];
|
|
6385
6700
|
function asGrant(row2) {
|
|
6386
6701
|
if (typeof row2.id !== "string" || typeof row2.emailNormalized !== "string") return null;
|
|
6387
6702
|
if (row2.sourceScope !== "local" && row2.sourceScope !== "parent") return null;
|
|
@@ -6392,7 +6707,7 @@ async function findAdmissionGrant(db, targetId, email, now) {
|
|
|
6392
6707
|
const emailNormalized = normalizeAdmissionEmail(email);
|
|
6393
6708
|
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
6394
6709
|
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
6395
|
-
const active =
|
|
6710
|
+
const active = rows3(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
6396
6711
|
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
6397
6712
|
)).sort((a, b) => {
|
|
6398
6713
|
const scope2 = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
@@ -6406,7 +6721,7 @@ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
|
6406
6721
|
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
6407
6722
|
}
|
|
6408
6723
|
const found = await db.query({ admissionGrantRevisions: {} });
|
|
6409
|
-
return
|
|
6724
|
+
return rows3(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
|
|
6410
6725
|
}
|
|
6411
6726
|
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
6412
6727
|
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
@@ -6415,7 +6730,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
|
|
|
6415
6730
|
order: { revision: "desc" },
|
|
6416
6731
|
limit: 1
|
|
6417
6732
|
} } });
|
|
6418
|
-
const row2 =
|
|
6733
|
+
const row2 = rows3(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
6419
6734
|
return row2 ? row2 : null;
|
|
6420
6735
|
}
|
|
6421
6736
|
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
@@ -6494,8 +6809,8 @@ var APPLICATION_LABELS = {
|
|
|
6494
6809
|
state: "State"
|
|
6495
6810
|
};
|
|
6496
6811
|
function humanize(id7) {
|
|
6497
|
-
const
|
|
6498
|
-
return
|
|
6812
|
+
const text11 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6813
|
+
return text11 ? `${text11[0].toUpperCase()}${text11.slice(1)}` : id7;
|
|
6499
6814
|
}
|
|
6500
6815
|
function applicationDefs(chapter) {
|
|
6501
6816
|
try {
|
|
@@ -6588,8 +6903,8 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6588
6903
|
errors.push(`${path} must be an object`);
|
|
6589
6904
|
continue;
|
|
6590
6905
|
}
|
|
6591
|
-
const
|
|
6592
|
-
for (const key3 of Object.keys(item)) if (!
|
|
6906
|
+
const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6907
|
+
for (const key3 of Object.keys(item)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6593
6908
|
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
6594
6909
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6595
6910
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
@@ -6653,27 +6968,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
6653
6968
|
if (db.aggregate) {
|
|
6654
6969
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
6655
6970
|
}
|
|
6656
|
-
const
|
|
6657
|
-
return Array.isArray(
|
|
6971
|
+
const rows6 = (await db.query({ signupControls: {} })).signupControls;
|
|
6972
|
+
return Array.isArray(rows6) && rows6.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
|
|
6658
6973
|
}
|
|
6659
6974
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
6660
6975
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
6661
|
-
const
|
|
6976
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
6662
6977
|
where: { targetId, environment },
|
|
6663
6978
|
order: { revision: "desc" },
|
|
6664
6979
|
limit: 1
|
|
6665
6980
|
} } })).signupControls;
|
|
6666
|
-
const latest = Array.isArray(
|
|
6981
|
+
const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
6667
6982
|
return asStored(latest);
|
|
6668
6983
|
}
|
|
6669
6984
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
6670
6985
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
6671
|
-
const
|
|
6986
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
6672
6987
|
where: { targetId, environment, active: true },
|
|
6673
6988
|
order: { revision: "desc" },
|
|
6674
6989
|
limit: 1
|
|
6675
6990
|
} } })).signupControls;
|
|
6676
|
-
const active = Array.isArray(
|
|
6991
|
+
const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
6677
6992
|
return asStored(active);
|
|
6678
6993
|
}
|
|
6679
6994
|
function controlledApplication(base, document) {
|
|
@@ -7046,17 +7361,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
7046
7361
|
if (!reader) return "";
|
|
7047
7362
|
const decoder = new TextDecoder();
|
|
7048
7363
|
let bytes = 0;
|
|
7049
|
-
let
|
|
7364
|
+
let text11 = "";
|
|
7050
7365
|
for (; ; ) {
|
|
7051
7366
|
const { done, value } = await reader.read();
|
|
7052
7367
|
if (req.signal.aborted) return invalid2();
|
|
7053
7368
|
if (done) {
|
|
7054
7369
|
complete = true;
|
|
7055
|
-
return
|
|
7370
|
+
return text11 + decoder.decode();
|
|
7056
7371
|
}
|
|
7057
7372
|
bytes += value.byteLength;
|
|
7058
7373
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
7059
|
-
|
|
7374
|
+
text11 += decoder.decode(value, { stream: true });
|
|
7060
7375
|
}
|
|
7061
7376
|
} catch {
|
|
7062
7377
|
return invalid2();
|
|
@@ -7160,9 +7475,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
7160
7475
|
let selection;
|
|
7161
7476
|
if (request?.body) {
|
|
7162
7477
|
try {
|
|
7163
|
-
const
|
|
7164
|
-
if (
|
|
7165
|
-
const body = JSON.parse(
|
|
7478
|
+
const text11 = await readBoundedRequestText(request, 2048);
|
|
7479
|
+
if (text11 instanceof Response) return text11;
|
|
7480
|
+
const body = JSON.parse(text11);
|
|
7166
7481
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
7167
7482
|
selection = body.namedSeat;
|
|
7168
7483
|
} catch {
|
|
@@ -7315,9 +7630,9 @@ async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
|
|
|
7315
7630
|
if (!app || app.groupId !== chapterId || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(runtime)) {
|
|
7316
7631
|
throw new Error("invitation purchaser unavailable");
|
|
7317
7632
|
}
|
|
7318
|
-
const
|
|
7319
|
-
if (!
|
|
7320
|
-
return
|
|
7633
|
+
const name2 = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
|
|
7634
|
+
if (!name2) throw new Error("invitation purchaser name unavailable");
|
|
7635
|
+
return name2.slice(0, 160);
|
|
7321
7636
|
}
|
|
7322
7637
|
async function namedSeatApplicationContext(req, seatId, env, ctx) {
|
|
7323
7638
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
@@ -7346,10 +7661,10 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
|
|
|
7346
7661
|
}
|
|
7347
7662
|
async function existingNamedSeatApplication(context, env, ctx) {
|
|
7348
7663
|
const { db, userId, email, invitation } = context;
|
|
7349
|
-
const
|
|
7350
|
-
const apps =
|
|
7664
|
+
const rows6 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
|
|
7665
|
+
const apps = rows6.filter((row2) => !closedApplication(row2));
|
|
7351
7666
|
if (!apps.length) {
|
|
7352
|
-
const closed =
|
|
7667
|
+
const closed = rows6.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
7353
7668
|
return { predecessor: closed ? { id: String(closed.id) } : null };
|
|
7354
7669
|
}
|
|
7355
7670
|
const app = apps[0];
|
|
@@ -7714,6 +8029,111 @@ async function notifySubmitConfirmed(db, env, notice) {
|
|
|
7714
8029
|
}
|
|
7715
8030
|
}
|
|
7716
8031
|
|
|
8032
|
+
// src/member-entitlement.ts
|
|
8033
|
+
function entitledTierId(app, now = Date.now()) {
|
|
8034
|
+
const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
|
|
8035
|
+
const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
|
|
8036
|
+
if (granted && (endsAt === null || endsAt > now)) return granted;
|
|
8037
|
+
return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
|
|
8038
|
+
}
|
|
8039
|
+
|
|
8040
|
+
// src/portal-member.ts
|
|
8041
|
+
var text6 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8042
|
+
async function activeHubGrant(db, identity2) {
|
|
8043
|
+
const byUser = (await db.query({ hubMemberGrants: { $: { where: { userId: identity2.userId }, limit: 5 } } })).hubMemberGrants ?? [];
|
|
8044
|
+
const byEmail = identity2.email ? (await db.query({ hubMemberGrants: { $: { where: { emailNormalized: normalizeChapterEmail(identity2.email) }, limit: 1 } } })).hubMemberGrants ?? [] : [];
|
|
8045
|
+
return [...byUser, ...byEmail].find((row2) => row2.status === "active") ?? null;
|
|
8046
|
+
}
|
|
8047
|
+
async function resolvePortalMember(db, env, ctx, identity2) {
|
|
8048
|
+
try {
|
|
8049
|
+
if (ctx.chapter.mode === "hub") {
|
|
8050
|
+
const grant = await activeHubGrant(db, identity2);
|
|
8051
|
+
if (!grant || !identity2.email) return { status: "not-member" };
|
|
8052
|
+
return { status: "member", member: {
|
|
8053
|
+
memberId: String(grant.id),
|
|
8054
|
+
source: "hub-grant",
|
|
8055
|
+
email: identity2.email,
|
|
8056
|
+
...text6(grant.firstName) ? { firstName: text6(grant.firstName) } : {},
|
|
8057
|
+
...text6(grant.lastName) ? { lastName: text6(grant.lastName) } : {},
|
|
8058
|
+
tierId: null,
|
|
8059
|
+
memberSince: Number(grant.grantedAt) || null,
|
|
8060
|
+
row: grant
|
|
8061
|
+
} };
|
|
8062
|
+
}
|
|
8063
|
+
const session = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, identity2);
|
|
8064
|
+
if (!session) return { status: "not-member" };
|
|
8065
|
+
const app = session.source;
|
|
8066
|
+
if (!await chapterMembershipAccess(db, env, ctx, app)) return { status: "not-member", row: app };
|
|
8067
|
+
return { status: "member", member: {
|
|
8068
|
+
memberId: String(app.id),
|
|
8069
|
+
source: "application",
|
|
8070
|
+
email: String(app.email ?? identity2.email ?? ""),
|
|
8071
|
+
...text6(app.firstName) ? { firstName: text6(app.firstName) } : {},
|
|
8072
|
+
...text6(app.lastName) ? { lastName: text6(app.lastName) } : {},
|
|
8073
|
+
tierId: entitledTierId(app),
|
|
8074
|
+
memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
|
|
8075
|
+
row: app
|
|
8076
|
+
} };
|
|
8077
|
+
} catch {
|
|
8078
|
+
return { status: "unavailable" };
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
function initialsOf(name2) {
|
|
8082
|
+
const words2 = name2.trim().split(/\s+/).filter(Boolean);
|
|
8083
|
+
const letters = words2.length > 1 ? [words2[0], words2[words2.length - 1]] : words2;
|
|
8084
|
+
return letters.map((word) => [...word][0].toUpperCase()).join("") || "?";
|
|
8085
|
+
}
|
|
8086
|
+
async function portalIdentity(db, ctx, member) {
|
|
8087
|
+
const profile = (await db.query({ memberProfiles: { $: { where: { id: member.memberId }, limit: 1 } } })).memberProfiles?.[0];
|
|
8088
|
+
let tier = null;
|
|
8089
|
+
if (member.tierId) {
|
|
8090
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
8091
|
+
const match = group ? (await loadTiers(db, group)).find((candidate) => candidate.id === member.tierId) : void 0;
|
|
8092
|
+
tier = { id: member.tierId, name: match?.name ?? member.tierId };
|
|
8093
|
+
}
|
|
8094
|
+
const legal = [member.firstName, member.lastName].filter(Boolean).join(" ");
|
|
8095
|
+
const displayName = text6(profile?.preferredName) ?? (legal || member.email);
|
|
8096
|
+
const photoPath = text6(profile?.photoPath);
|
|
8097
|
+
const photoUrl = photoPath && db.storage && ctx.chapter.memberPortal?.photos ? await db.storage.sign(photoPath, 3600).catch(() => null) : null;
|
|
8098
|
+
return {
|
|
8099
|
+
memberId: member.memberId,
|
|
8100
|
+
tier,
|
|
8101
|
+
memberSince: member.memberSince,
|
|
8102
|
+
displayName,
|
|
8103
|
+
firstName: text6(profile?.preferredName)?.split(/\s+/)[0] ?? member.firstName ?? null,
|
|
8104
|
+
initials: initialsOf(displayName),
|
|
8105
|
+
location: text6(profile?.location) ?? null,
|
|
8106
|
+
photoUrl
|
|
8107
|
+
};
|
|
8108
|
+
}
|
|
8109
|
+
|
|
8110
|
+
// src/portal-me.ts
|
|
8111
|
+
var servesMembers = (ctx) => ctx.chapter.mode === "chapter" || Boolean(ctx.chapter.memberPortal || ctx.chapter.memberContent);
|
|
8112
|
+
async function hubMe(db, env, ctx, identity2, base) {
|
|
8113
|
+
if (!servesMembers(ctx)) return base;
|
|
8114
|
+
const result = await resolvePortalMember(db, env, ctx, identity2);
|
|
8115
|
+
if (result.status === "unavailable") return { ...base, memberAccess: null, portal: null };
|
|
8116
|
+
if (result.status !== "member") return { ...base, memberAccess: false, portal: null };
|
|
8117
|
+
return { ...base, memberAccess: true, portal: await identityOrNull(db, ctx, result.member) };
|
|
8118
|
+
}
|
|
8119
|
+
async function chapterPortalIdentity(db, ctx, memberAccess, app) {
|
|
8120
|
+
if (!memberAccess || !app || !ctx.chapter.memberPortal) return null;
|
|
8121
|
+
const text11 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8122
|
+
return identityOrNull(db, ctx, {
|
|
8123
|
+
memberId: String(app.id),
|
|
8124
|
+
source: "application",
|
|
8125
|
+
email: String(app.email ?? ""),
|
|
8126
|
+
...text11(app.firstName) ? { firstName: text11(app.firstName) } : {},
|
|
8127
|
+
...text11(app.lastName) ? { lastName: text11(app.lastName) } : {},
|
|
8128
|
+
tierId: entitledTierId(app),
|
|
8129
|
+
memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
|
|
8130
|
+
row: app
|
|
8131
|
+
});
|
|
8132
|
+
}
|
|
8133
|
+
async function identityOrNull(db, ctx, member) {
|
|
8134
|
+
return portalIdentity(db, ctx, member).catch(() => null);
|
|
8135
|
+
}
|
|
8136
|
+
|
|
7717
8137
|
// src/worker-routes-health.ts
|
|
7718
8138
|
var handleConfig = async (_req, url, env, ctx) => {
|
|
7719
8139
|
if (url.pathname !== "/api/config") return null;
|
|
@@ -7728,13 +8148,13 @@ var handleHealth = async (_req, url, env, ctx) => {
|
|
|
7728
8148
|
if (url.pathname !== "/api/health") return null;
|
|
7729
8149
|
const response = json({ ok: true });
|
|
7730
8150
|
response.headers.set("cache-control", "no-store");
|
|
7731
|
-
for (const [
|
|
8151
|
+
for (const [name2, value] of Object.entries({
|
|
7732
8152
|
"x-odla-app-id": env.ODLA_APP_ID,
|
|
7733
8153
|
"x-odla-chapter-id": ctx.chapter.id,
|
|
7734
8154
|
"x-odla-data-environment": env.ODLA_ENV,
|
|
7735
8155
|
"x-odla-runtime": env.ODLA_RUNTIME || "default"
|
|
7736
8156
|
})) {
|
|
7737
|
-
if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(
|
|
8157
|
+
if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name2, value);
|
|
7738
8158
|
}
|
|
7739
8159
|
return response;
|
|
7740
8160
|
};
|
|
@@ -7748,8 +8168,10 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
7748
8168
|
const db = ctx.makeDb(env);
|
|
7749
8169
|
const role = await ctx.roleFor(db, u);
|
|
7750
8170
|
const superAdmin = await ctx.isSuperAdminEmail(db, u.email, u.userId);
|
|
7751
|
-
const
|
|
7752
|
-
|
|
8171
|
+
const admin = superAdmin || isAdminRole(role, ctx.auth);
|
|
8172
|
+
const secondFactorOk = !admin || adminSecondFactorAllows(ctx.auth.adminSecondFactor ?? "off", u.payload, u.email);
|
|
8173
|
+
const base = { authorized: admin && secondFactorOk, role, superAdmin, email: u.email ?? null, ...admin && !secondFactorOk ? { secondFactorRequired: true } : {} };
|
|
8174
|
+
if (ctx.chapter.mode !== "chapter") return json(await hubMe(db, env, ctx, u, base));
|
|
7753
8175
|
let member;
|
|
7754
8176
|
try {
|
|
7755
8177
|
member = await memberSessionApplication(
|
|
@@ -7785,7 +8207,8 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
7785
8207
|
} catch {
|
|
7786
8208
|
}
|
|
7787
8209
|
}
|
|
7788
|
-
|
|
8210
|
+
const portal = await chapterPortalIdentity(db, ctx, memberAccess, member?.source);
|
|
8211
|
+
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, portal, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
|
|
7789
8212
|
};
|
|
7790
8213
|
var handleCrm = async (req, url, env, ctx) => {
|
|
7791
8214
|
const crmBase = ctx.crmBase;
|
|
@@ -8030,9 +8453,9 @@ var MAX_BENEFITS = 24;
|
|
|
8030
8453
|
var MAX_MARKDOWN = 65536;
|
|
8031
8454
|
var MAX_LABEL = 160;
|
|
8032
8455
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8033
|
-
function rejectUnknownKeys(value,
|
|
8456
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
8034
8457
|
for (const key3 of Object.keys(value)) {
|
|
8035
|
-
if (!
|
|
8458
|
+
if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8036
8459
|
}
|
|
8037
8460
|
}
|
|
8038
8461
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -8044,10 +8467,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
8044
8467
|
errors.push(`${path} must be a string`);
|
|
8045
8468
|
return void 0;
|
|
8046
8469
|
}
|
|
8047
|
-
const
|
|
8048
|
-
if (required && !
|
|
8049
|
-
if (
|
|
8050
|
-
return
|
|
8470
|
+
const text11 = value.trim();
|
|
8471
|
+
if (required && !text11) errors.push(`${path} is required`);
|
|
8472
|
+
if (text11.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
8473
|
+
return text11;
|
|
8051
8474
|
}
|
|
8052
8475
|
function validateMembershipPageDocument(input, options = {}) {
|
|
8053
8476
|
const errors = [];
|
|
@@ -8096,8 +8519,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
8096
8519
|
else {
|
|
8097
8520
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
8098
8521
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
8099
|
-
const
|
|
8100
|
-
return
|
|
8522
|
+
const text11 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8523
|
+
return text11 ? [text11] : [];
|
|
8101
8524
|
});
|
|
8102
8525
|
}
|
|
8103
8526
|
}
|
|
@@ -8188,10 +8611,10 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
8188
8611
|
]);
|
|
8189
8612
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
|
|
8190
8613
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8191
|
-
function unknownKeys(value,
|
|
8192
|
-
for (const key3 of Object.keys(value)) if (!
|
|
8614
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
8615
|
+
for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8193
8616
|
}
|
|
8194
|
-
function
|
|
8617
|
+
function text7(value, path, errors, max = 65536) {
|
|
8195
8618
|
if (typeof value !== "string" || !value.trim()) {
|
|
8196
8619
|
errors.push(`${path} is required`);
|
|
8197
8620
|
return "";
|
|
@@ -8214,11 +8637,11 @@ function tiersOf(value, errors) {
|
|
|
8214
8637
|
return [];
|
|
8215
8638
|
}
|
|
8216
8639
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
8217
|
-
const id7 =
|
|
8640
|
+
const id7 = text7(item.id, `${path}.id`, errors, 80);
|
|
8218
8641
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8219
8642
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
8220
8643
|
seen.add(id7);
|
|
8221
|
-
const
|
|
8644
|
+
const name2 = text7(item.name, `${path}.name`, errors, 160);
|
|
8222
8645
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
8223
8646
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
8224
8647
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -8236,10 +8659,10 @@ function tiersOf(value, errors) {
|
|
|
8236
8659
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
8237
8660
|
errors.push(`${path}.interval must be year or month`);
|
|
8238
8661
|
}
|
|
8239
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8662
|
+
const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
8240
8663
|
return [{
|
|
8241
8664
|
id: id7,
|
|
8242
|
-
name,
|
|
8665
|
+
name: name2,
|
|
8243
8666
|
available: item.available === true,
|
|
8244
8667
|
sortOrder: Number(item.sortOrder) || 0,
|
|
8245
8668
|
priceCents: Number(item.priceCents) || 0,
|
|
@@ -8282,7 +8705,7 @@ function policyOf(value, errors, tiers) {
|
|
|
8282
8705
|
errors.push("document.policy must be an object");
|
|
8283
8706
|
return void 0;
|
|
8284
8707
|
}
|
|
8285
|
-
const
|
|
8708
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
8286
8709
|
"disclaimerText",
|
|
8287
8710
|
"refundPolicyText",
|
|
8288
8711
|
"merchantDisclosureText",
|
|
@@ -8290,11 +8713,11 @@ function policyOf(value, errors, tiers) {
|
|
|
8290
8713
|
"commitmentText",
|
|
8291
8714
|
"normsText"
|
|
8292
8715
|
]);
|
|
8293
|
-
unknownKeys(input, /* @__PURE__ */ new Set([...
|
|
8716
|
+
unknownKeys(input, /* @__PURE__ */ new Set([...allowed, "tierPolicies"]), "document.policy", errors);
|
|
8294
8717
|
const result = {};
|
|
8295
|
-
for (const key3 of
|
|
8718
|
+
for (const key3 of allowed) {
|
|
8296
8719
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
8297
|
-
result[key3] =
|
|
8720
|
+
result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
|
|
8298
8721
|
}
|
|
8299
8722
|
const tierPolicies = {};
|
|
8300
8723
|
if (input.tierPolicies !== void 0) {
|
|
@@ -8309,8 +8732,8 @@ function policyOf(value, errors, tiers) {
|
|
|
8309
8732
|
}
|
|
8310
8733
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
8311
8734
|
tierPolicies[id7] = {
|
|
8312
|
-
refundPolicyText:
|
|
8313
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8735
|
+
refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8736
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
8314
8737
|
};
|
|
8315
8738
|
}
|
|
8316
8739
|
}
|
|
@@ -8350,9 +8773,9 @@ function validateSignupControlDocument(input) {
|
|
|
8350
8773
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
8351
8774
|
const source = recordOf(root.source);
|
|
8352
8775
|
const target = recordOf(root.target);
|
|
8353
|
-
const appId =
|
|
8354
|
-
const chapterId =
|
|
8355
|
-
const environment =
|
|
8776
|
+
const appId = text7(source?.appId, "document.source.appId", errors, 63);
|
|
8777
|
+
const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8778
|
+
const environment = text7(target?.environment, "document.target.environment", errors, 63);
|
|
8356
8779
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
8357
8780
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
8358
8781
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -8493,14 +8916,45 @@ async function memberContentDigest(document) {
|
|
|
8493
8916
|
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
8494
8917
|
}
|
|
8495
8918
|
|
|
8919
|
+
// src/member-content-kinds.ts
|
|
8920
|
+
var MEMBER_PAGE_FEATURES = ["course", "share"];
|
|
8921
|
+
var MEMBER_PAGE_ICONS = [
|
|
8922
|
+
"home",
|
|
8923
|
+
"course",
|
|
8924
|
+
"library",
|
|
8925
|
+
"deals",
|
|
8926
|
+
"training",
|
|
8927
|
+
"events",
|
|
8928
|
+
"members",
|
|
8929
|
+
"share",
|
|
8930
|
+
"profile",
|
|
8931
|
+
"document",
|
|
8932
|
+
"calendar",
|
|
8933
|
+
"chat",
|
|
8934
|
+
"star"
|
|
8935
|
+
];
|
|
8936
|
+
var MEMBER_PAGE_TAGS = ["new"];
|
|
8937
|
+
function composeMemberSections(parent, chapter, pages) {
|
|
8938
|
+
const parentDeclared = new Map((parent?.sections ?? []).map((section) => [section.id, section]));
|
|
8939
|
+
const out = /* @__PURE__ */ new Map();
|
|
8940
|
+
for (const section of chapter?.sections ?? []) {
|
|
8941
|
+
const label = section.label ?? parentDeclared.get(section.id)?.label;
|
|
8942
|
+
out.set(section.id, { id: section.id, ...label ? { label } : {} });
|
|
8943
|
+
}
|
|
8944
|
+
for (const section of parent?.sections ?? []) if (!out.has(section.id)) out.set(section.id, section);
|
|
8945
|
+
const named = [...new Set(pages.map((page) => page.navigation.section))];
|
|
8946
|
+
for (const id7 of named.filter((id8) => !out.has(id8)).sort((a, b) => a.localeCompare(b))) out.set(id7, { id: id7, label: id7 });
|
|
8947
|
+
return [...out.values()].filter((section) => named.includes(section.id));
|
|
8948
|
+
}
|
|
8949
|
+
|
|
8496
8950
|
// src/member-content-validation-helpers.ts
|
|
8497
8951
|
var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
8498
8952
|
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8499
|
-
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
8953
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides", "sections"]);
|
|
8500
8954
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
8501
8955
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
8502
|
-
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
8503
|
-
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
8956
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell", "kind", "placeholder", "feature"]);
|
|
8957
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId", "tag", "icon"]);
|
|
8504
8958
|
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
8505
8959
|
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
8506
8960
|
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
@@ -8509,8 +8963,8 @@ var MAX_PAGES = 100;
|
|
|
8509
8963
|
var MAX_MARKDOWN2 = 65536;
|
|
8510
8964
|
var MAX_TEXT = 240;
|
|
8511
8965
|
var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8512
|
-
function unknownKeys2(value,
|
|
8513
|
-
for (const key3 of Object.keys(value)) if (!
|
|
8966
|
+
function unknownKeys2(value, allowed, path, errors) {
|
|
8967
|
+
for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8514
8968
|
}
|
|
8515
8969
|
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
8516
8970
|
if (value === void 0) {
|
|
@@ -8550,13 +9004,140 @@ function offerValue(value, path, errors, tiers, audience) {
|
|
|
8550
9004
|
};
|
|
8551
9005
|
}
|
|
8552
9006
|
|
|
9007
|
+
// src/member-content-validation-v2.ts
|
|
9008
|
+
var SECTION_KEYS = /* @__PURE__ */ new Set(["id", "label"]);
|
|
9009
|
+
var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set(["availableYear", "badge", "actionLabel"]);
|
|
9010
|
+
var FEATURE_KEYS = /* @__PURE__ */ new Set(["name", "ref"]);
|
|
9011
|
+
var MAX_SECTIONS = 24;
|
|
9012
|
+
var V2_ROOT_KEYS = ["sections"];
|
|
9013
|
+
var V2_PAGE_KEYS = ["kind", "placeholder", "feature"];
|
|
9014
|
+
var V2_NAV_KEYS = ["tag", "icon"];
|
|
9015
|
+
function rejectV2Fields(raw2, keys3, path, errors) {
|
|
9016
|
+
for (const key3 of keys3) if (raw2[key3] !== void 0) errors.push(`${path}.${key3} requires schemaVersion 2`);
|
|
9017
|
+
}
|
|
9018
|
+
function sectionsValue(value, errors) {
|
|
9019
|
+
if (value === void 0) return void 0;
|
|
9020
|
+
if (!Array.isArray(value)) {
|
|
9021
|
+
errors.push("document.sections must be an array");
|
|
9022
|
+
return void 0;
|
|
9023
|
+
}
|
|
9024
|
+
if (value.length > MAX_SECTIONS) errors.push(`document.sections supports at most ${MAX_SECTIONS} entries`);
|
|
9025
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9026
|
+
const sections = [];
|
|
9027
|
+
for (const [index2, item] of value.slice(0, MAX_SECTIONS).entries()) {
|
|
9028
|
+
const path = `document.sections[${index2}]`;
|
|
9029
|
+
const raw2 = record2(item);
|
|
9030
|
+
if (!raw2) {
|
|
9031
|
+
errors.push(`${path} must be an object`);
|
|
9032
|
+
continue;
|
|
9033
|
+
}
|
|
9034
|
+
unknownKeys2(raw2, SECTION_KEYS, path, errors);
|
|
9035
|
+
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
9036
|
+
if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9037
|
+
if (seen.has(id7)) errors.push(`${path}.id duplicates section "${id7}"`);
|
|
9038
|
+
seen.add(id7);
|
|
9039
|
+
const label = textValue(raw2.label, `${path}.label`, errors, 120);
|
|
9040
|
+
sections.push({ id: id7, ...label ? { label } : {} });
|
|
9041
|
+
}
|
|
9042
|
+
return sections;
|
|
9043
|
+
}
|
|
9044
|
+
var oneOf = (values, value) => typeof value === "string" && values.includes(value);
|
|
9045
|
+
function navigationV2(raw2, path, errors) {
|
|
9046
|
+
const out = {};
|
|
9047
|
+
if (raw2.tag !== void 0) {
|
|
9048
|
+
if (oneOf(MEMBER_PAGE_TAGS, raw2.tag)) out.tag = raw2.tag;
|
|
9049
|
+
else errors.push(`${path}.tag must be one of ${MEMBER_PAGE_TAGS.join(", ")}`);
|
|
9050
|
+
}
|
|
9051
|
+
if (raw2.icon !== void 0) {
|
|
9052
|
+
if (oneOf(MEMBER_PAGE_ICONS, raw2.icon)) out.icon = raw2.icon;
|
|
9053
|
+
else errors.push(`${path}.icon must be one of ${MEMBER_PAGE_ICONS.join(", ")}`);
|
|
9054
|
+
}
|
|
9055
|
+
return out;
|
|
9056
|
+
}
|
|
9057
|
+
function placeholderValue(value, path, errors) {
|
|
9058
|
+
const raw2 = record2(value);
|
|
9059
|
+
if (!raw2) {
|
|
9060
|
+
errors.push(`${path} must be an object`);
|
|
9061
|
+
return {};
|
|
9062
|
+
}
|
|
9063
|
+
unknownKeys2(raw2, PLACEHOLDER_KEYS, path, errors);
|
|
9064
|
+
const year = raw2.availableYear;
|
|
9065
|
+
if (year !== void 0 && (!Number.isInteger(year) || Number(year) < 2e3 || Number(year) > 2100)) {
|
|
9066
|
+
errors.push(`${path}.availableYear must be a year from 2000 to 2100`);
|
|
9067
|
+
}
|
|
9068
|
+
const badge = textValue(raw2.badge, `${path}.badge`, errors, 60);
|
|
9069
|
+
const actionLabel = textValue(raw2.actionLabel, `${path}.actionLabel`, errors, 80);
|
|
9070
|
+
return {
|
|
9071
|
+
...Number.isInteger(year) ? { availableYear: Number(year) } : {},
|
|
9072
|
+
...badge ? { badge } : {},
|
|
9073
|
+
...actionLabel ? { actionLabel } : {}
|
|
9074
|
+
};
|
|
9075
|
+
}
|
|
9076
|
+
function featureValue(value, path, errors) {
|
|
9077
|
+
const raw2 = record2(value);
|
|
9078
|
+
if (!raw2) {
|
|
9079
|
+
errors.push(`${path} must be an object`);
|
|
9080
|
+
return void 0;
|
|
9081
|
+
}
|
|
9082
|
+
unknownKeys2(raw2, FEATURE_KEYS, path, errors);
|
|
9083
|
+
if (!oneOf(MEMBER_PAGE_FEATURES, raw2.name)) {
|
|
9084
|
+
errors.push(`${path}.name must be one of ${MEMBER_PAGE_FEATURES.join(", ")}`);
|
|
9085
|
+
return void 0;
|
|
9086
|
+
}
|
|
9087
|
+
const ref = textValue(raw2.ref, `${path}.ref`, errors, 80);
|
|
9088
|
+
if (ref && !SLUG2.test(ref)) errors.push(`${path}.ref must be a lowercase slug`);
|
|
9089
|
+
return { name: raw2.name, ...ref ? { ref } : {} };
|
|
9090
|
+
}
|
|
9091
|
+
function pageKindValue(raw2, path, errors) {
|
|
9092
|
+
const kind = raw2.kind ?? "markdown";
|
|
9093
|
+
if (kind !== "markdown" && kind !== "placeholder" && kind !== "feature") {
|
|
9094
|
+
errors.push(`${path}.kind must be markdown, placeholder or feature`);
|
|
9095
|
+
return {};
|
|
9096
|
+
}
|
|
9097
|
+
if (kind !== "placeholder" && raw2.placeholder !== void 0) errors.push(`${path}.placeholder requires kind placeholder`);
|
|
9098
|
+
if (kind !== "feature" && raw2.feature !== void 0) errors.push(`${path}.feature requires kind feature`);
|
|
9099
|
+
if (kind === "markdown" && raw2.bodyMarkdown === void 0) errors.push(`${path}.bodyMarkdown is required`);
|
|
9100
|
+
if (kind === "feature" && raw2.bodyMarkdown !== void 0) errors.push(`${path}.bodyMarkdown is not supported on a feature page`);
|
|
9101
|
+
const out = raw2.kind === void 0 ? {} : { kind };
|
|
9102
|
+
if (kind === "placeholder") out.placeholder = placeholderValue(raw2.placeholder ?? {}, `${path}.placeholder`, errors);
|
|
9103
|
+
if (kind === "feature") {
|
|
9104
|
+
if (raw2.feature === void 0) errors.push(`${path}.feature is required for kind feature`);
|
|
9105
|
+
else {
|
|
9106
|
+
const feature = featureValue(raw2.feature, `${path}.feature`, errors);
|
|
9107
|
+
if (feature) out.feature = feature;
|
|
9108
|
+
}
|
|
9109
|
+
}
|
|
9110
|
+
return out;
|
|
9111
|
+
}
|
|
9112
|
+
function duplicateFeatures(pages, errors) {
|
|
9113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9114
|
+
for (const [index2, page] of pages.entries()) {
|
|
9115
|
+
if (!page.feature) continue;
|
|
9116
|
+
const key3 = `${page.feature.name}:${page.feature.ref ?? ""}`;
|
|
9117
|
+
if (seen.has(key3)) errors.push(`document.pages[${index2}].feature duplicates ${page.feature.name}${page.feature.ref ? ` "${page.feature.ref}"` : ""}`);
|
|
9118
|
+
seen.add(key3);
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
function sectionReferences(pages, sections, parentPages, errors) {
|
|
9122
|
+
if (!sections) return;
|
|
9123
|
+
const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), ...parentPages.map((page) => page.navigation.section)]);
|
|
9124
|
+
for (const [index2, page] of pages.entries()) {
|
|
9125
|
+
if (!known.has(page.navigation.section)) {
|
|
9126
|
+
errors.push(`document.pages[${index2}].navigation.section references undeclared section "${page.navigation.section}"`);
|
|
9127
|
+
}
|
|
9128
|
+
}
|
|
9129
|
+
}
|
|
9130
|
+
|
|
8553
9131
|
// src/member-content-validation.ts
|
|
8554
9132
|
function validateMemberContentDocument(input, options = {}) {
|
|
8555
9133
|
const errors = [];
|
|
8556
9134
|
const root = record2(input);
|
|
8557
9135
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
8558
9136
|
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
8559
|
-
if (root.schemaVersion !==
|
|
9137
|
+
if (root.schemaVersion !== 1 && root.schemaVersion !== 2) errors.push("document.schemaVersion must be 1 or 2");
|
|
9138
|
+
const v2 = root.schemaVersion === 2;
|
|
9139
|
+
if (!v2) rejectV2Fields(root, V2_ROOT_KEYS, "document", errors);
|
|
9140
|
+
const sections = v2 ? sectionsValue(root.sections, errors) : void 0;
|
|
8560
9141
|
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
8561
9142
|
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
8562
9143
|
const sourceRaw = record2(root.source);
|
|
@@ -8601,6 +9182,8 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8601
9182
|
continue;
|
|
8602
9183
|
}
|
|
8603
9184
|
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
9185
|
+
if (!v2) rejectV2Fields(raw2, V2_PAGE_KEYS, path, errors);
|
|
9186
|
+
const kindFields = v2 ? pageKindValue(raw2, path, errors) : {};
|
|
8604
9187
|
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
8605
9188
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
8606
9189
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
@@ -8615,13 +9198,19 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8615
9198
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
8616
9199
|
else {
|
|
8617
9200
|
unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
9201
|
+
if (!v2) rejectV2Fields(navRaw, V2_NAV_KEYS, `${path}.navigation`, errors);
|
|
8618
9202
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
8619
9203
|
const order2 = navRaw.order;
|
|
8620
9204
|
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
8621
9205
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
8622
9206
|
}
|
|
8623
9207
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
8624
|
-
navigation = {
|
|
9208
|
+
navigation = {
|
|
9209
|
+
section,
|
|
9210
|
+
order: Number.isInteger(order2) ? Number(order2) : index2,
|
|
9211
|
+
...parentId ? { parentId } : {},
|
|
9212
|
+
...v2 ? navigationV2(navRaw, `${path}.navigation`, errors) : {}
|
|
9213
|
+
};
|
|
8625
9214
|
}
|
|
8626
9215
|
const audienceRaw = record2(raw2.audience);
|
|
8627
9216
|
let audience = { kind: "all-members" };
|
|
@@ -8647,7 +9236,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8647
9236
|
}
|
|
8648
9237
|
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
8649
9238
|
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
|
|
8650
|
-
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2,
|
|
9239
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, !v2);
|
|
8651
9240
|
let upsell;
|
|
8652
9241
|
if (raw2.upsell !== void 0) {
|
|
8653
9242
|
const upsellRaw = record2(raw2.upsell);
|
|
@@ -8664,7 +9253,22 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8664
9253
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
8665
9254
|
}
|
|
8666
9255
|
}
|
|
8667
|
-
pages.push({
|
|
9256
|
+
pages.push({
|
|
9257
|
+
id: id7,
|
|
9258
|
+
slug,
|
|
9259
|
+
title,
|
|
9260
|
+
...summary ? { summary } : {},
|
|
9261
|
+
navigation,
|
|
9262
|
+
audience,
|
|
9263
|
+
...teaserMarkdown ? { teaserMarkdown } : {},
|
|
9264
|
+
...bodyMarkdown !== void 0 ? { bodyMarkdown } : {},
|
|
9265
|
+
...upsell ? { upsell } : {},
|
|
9266
|
+
...kindFields
|
|
9267
|
+
});
|
|
9268
|
+
}
|
|
9269
|
+
if (v2) {
|
|
9270
|
+
duplicateFeatures(pages, errors);
|
|
9271
|
+
sectionReferences(pages, sections, options.parentPages ?? [], errors);
|
|
8668
9272
|
}
|
|
8669
9273
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
8670
9274
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -8711,21 +9315,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8711
9315
|
}
|
|
8712
9316
|
if (errors.length) return { ok: false, errors };
|
|
8713
9317
|
return { ok: true, value: {
|
|
8714
|
-
schemaVersion:
|
|
9318
|
+
schemaVersion: v2 ? 2 : 1,
|
|
8715
9319
|
revision: Number(root.revision),
|
|
8716
9320
|
generatedAt: Number(root.generatedAt),
|
|
8717
9321
|
source,
|
|
8718
9322
|
...target ? { target } : {},
|
|
8719
9323
|
pages,
|
|
8720
|
-
...upsellOverrides ? { upsellOverrides } : {}
|
|
9324
|
+
...upsellOverrides ? { upsellOverrides } : {},
|
|
9325
|
+
...sections ? { sections } : {}
|
|
8721
9326
|
} };
|
|
8722
9327
|
}
|
|
8723
9328
|
|
|
8724
9329
|
// src/member-content.ts
|
|
8725
|
-
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
8726
9330
|
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
8727
9331
|
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
8728
|
-
var pageOrder = (a, b) => a.navigation.section.
|
|
9332
|
+
var pageOrder = (rank) => (a, b) => (rank.get(a.navigation.section) ?? 0) - (rank.get(b.navigation.section) ?? 0) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
|
|
8729
9333
|
function composeMemberContent(parent, chapter) {
|
|
8730
9334
|
const pages = [];
|
|
8731
9335
|
const parentPages = /* @__PURE__ */ new Map();
|
|
@@ -8744,20 +9348,24 @@ function composeMemberContent(parent, chapter) {
|
|
|
8744
9348
|
}
|
|
8745
9349
|
pages.push({ ...page, source: chapter.source });
|
|
8746
9350
|
}
|
|
9351
|
+
const sections = composeMemberSections(parent, chapter, pages);
|
|
9352
|
+
const rank = new Map(sections.map((section, index2) => [section.id, index2]));
|
|
8747
9353
|
return {
|
|
8748
|
-
pages: pages.sort(pageOrder),
|
|
9354
|
+
pages: pages.sort(pageOrder(rank)),
|
|
9355
|
+
sections,
|
|
8749
9356
|
parentRevision: parent?.revision ?? null,
|
|
8750
9357
|
chapterRevision: chapter?.revision ?? null
|
|
8751
9358
|
};
|
|
8752
9359
|
}
|
|
8753
|
-
var
|
|
9360
|
+
var memberPageAllowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
8754
9361
|
function projectMemberContent(content, viewer, tiers) {
|
|
8755
9362
|
const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
8756
9363
|
return {
|
|
8757
9364
|
viewer: { ...viewer },
|
|
8758
9365
|
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
9366
|
+
sections: content.sections ?? composeMemberSections(null, null, content.pages),
|
|
8759
9367
|
pages: content.pages.map((page) => {
|
|
8760
|
-
const canRead =
|
|
9368
|
+
const canRead = memberPageAllowed(page, viewer);
|
|
8761
9369
|
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
8762
9370
|
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
8763
9371
|
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
@@ -8770,8 +9378,11 @@ function projectMemberContent(content, viewer, tiers) {
|
|
|
8770
9378
|
audience: page.audience,
|
|
8771
9379
|
source: page.source,
|
|
8772
9380
|
access: canRead ? "allowed" : "locked",
|
|
9381
|
+
kind: page.kind ?? "markdown",
|
|
9382
|
+
...page.placeholder ? { placeholder: page.placeholder } : {},
|
|
9383
|
+
...page.feature ? { feature: page.feature } : {},
|
|
8773
9384
|
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
8774
|
-
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
9385
|
+
...canRead && page.bodyMarkdown !== void 0 ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
8775
9386
|
...upsell ? { upsell } : {}
|
|
8776
9387
|
};
|
|
8777
9388
|
})
|
|
@@ -8783,33 +9394,33 @@ var control = (row2) => row2 && row2.document && typeof row2.document === "objec
|
|
|
8783
9394
|
var local2 = (row2) => row2 && row2.document && typeof row2.document === "object" ? row2 : null;
|
|
8784
9395
|
async function exists(db, ns, where) {
|
|
8785
9396
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
8786
|
-
const
|
|
8787
|
-
return
|
|
9397
|
+
const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
9398
|
+
return rows6.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
|
|
8788
9399
|
}
|
|
8789
9400
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
8790
9401
|
const where = { targetId, environment, active: true };
|
|
8791
9402
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
8792
|
-
const
|
|
9403
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
8793
9404
|
where,
|
|
8794
9405
|
order: { revision: "desc" },
|
|
8795
9406
|
limit: 1
|
|
8796
9407
|
} } })).memberContentControls;
|
|
8797
|
-
return control(Array.isArray(
|
|
9408
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8798
9409
|
}
|
|
8799
9410
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
8800
9411
|
const where = { targetId, environment };
|
|
8801
9412
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
8802
|
-
const
|
|
9413
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
8803
9414
|
where,
|
|
8804
9415
|
order: { revision: "desc" },
|
|
8805
9416
|
limit: 1
|
|
8806
9417
|
} } })).memberContentControls;
|
|
8807
|
-
return control(Array.isArray(
|
|
9418
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8808
9419
|
}
|
|
8809
9420
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
8810
9421
|
const id7 = `member-content-local:${environment}`;
|
|
8811
|
-
const
|
|
8812
|
-
return local2(Array.isArray(
|
|
9422
|
+
const rows6 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
|
|
9423
|
+
return local2(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
8813
9424
|
}
|
|
8814
9425
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
8815
9426
|
const { document, digest } = envelope;
|
|
@@ -8856,7 +9467,8 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
|
|
|
8856
9467
|
var ADMIN_PATH = "/api/admin/member-content";
|
|
8857
9468
|
var BODY_CAP = 1e6;
|
|
8858
9469
|
var reject4 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
|
|
8859
|
-
async function groupAndTiers(db, chapterId) {
|
|
9470
|
+
async function groupAndTiers(db, chapterId, mode = "chapter") {
|
|
9471
|
+
if (mode === "hub") return { group: null, tiers: [] };
|
|
8860
9472
|
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
8861
9473
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
8862
9474
|
if (!group) return { group: null, tiers: [] };
|
|
@@ -8893,7 +9505,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
8893
9505
|
if (document.source.appId !== settings.sourceId) return reject4("wrong_source", 403);
|
|
8894
9506
|
if (document.target?.chapterId !== ctx.chapter.id) return reject4("wrong_target", 409);
|
|
8895
9507
|
if (document.target.environment !== env.ODLA_ENV) return reject4("wrong_environment", 409);
|
|
8896
|
-
const { tiers } = await groupAndTiers(db, ctx.chapter.id);
|
|
9508
|
+
const { tiers } = await groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode);
|
|
8897
9509
|
const checked = validateMemberContentDocument(document, {
|
|
8898
9510
|
sourceType: "parent",
|
|
8899
9511
|
sourceAppId: settings.sourceId,
|
|
@@ -8947,17 +9559,13 @@ var handleMemberContent = async (req, url, env, ctx) => {
|
|
|
8947
9559
|
const user = await ctx.verifyUser(req, env);
|
|
8948
9560
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
8949
9561
|
const db = ctx.makeDb(env);
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
tierId = typeof member.source.tierId === "string" ? member.source.tierId : null;
|
|
8955
|
-
} catch {
|
|
8956
|
-
return json({ error: "membership access unavailable" }, 503);
|
|
8957
|
-
}
|
|
9562
|
+
const resolved = await resolvePortalMember(db, env, ctx, user);
|
|
9563
|
+
if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
|
|
9564
|
+
if (resolved.status !== "member") return json({ error: "member access required" }, 403);
|
|
9565
|
+
const tierId = resolved.member.tierId;
|
|
8958
9566
|
const [{ parent, chapter }, { group, tiers }] = await Promise.all([
|
|
8959
9567
|
activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
|
|
8960
|
-
groupAndTiers(db, ctx.chapter.id)
|
|
9568
|
+
groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
|
|
8961
9569
|
]);
|
|
8962
9570
|
const effective = composeMemberContent(parent?.document, chapter?.document);
|
|
8963
9571
|
let offerable = tiers;
|
|
@@ -8976,7 +9584,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
8976
9584
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
8977
9585
|
const [{ parent, chapter }, { tiers }] = await Promise.all([
|
|
8978
9586
|
activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
|
|
8979
|
-
groupAndTiers(db, ctx.chapter.id)
|
|
9587
|
+
groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
|
|
8980
9588
|
]);
|
|
8981
9589
|
if (req.method === "GET") {
|
|
8982
9590
|
const effective = composeMemberContent(parent?.document, chapter?.document);
|
|
@@ -8984,7 +9592,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
8984
9592
|
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
8985
9593
|
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
8986
9594
|
effective,
|
|
8987
|
-
tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
|
|
9595
|
+
tiers: tiers.map(({ id: id7, name: name2, priceCents, active }) => ({ id: id7, name: name2, priceCents, active })),
|
|
8988
9596
|
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
8989
9597
|
});
|
|
8990
9598
|
}
|
|
@@ -9011,6 +9619,8 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
9011
9619
|
parentPages: parent?.document.pages ?? []
|
|
9012
9620
|
});
|
|
9013
9621
|
if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
|
|
9622
|
+
const tiered = ctx.chapter.mode === "hub" ? checked.value.pages.findIndex((page) => page.audience.kind === "tiers") : -1;
|
|
9623
|
+
if (tiered >= 0) return json({ error: "invalid member content", errors: [`document.pages[${tiered}].audience must be all-members: a hub has no tiers`] }, 400);
|
|
9014
9624
|
try {
|
|
9015
9625
|
const saved = await saveLocalMemberContent(
|
|
9016
9626
|
db,
|
|
@@ -9032,13 +9642,521 @@ function withLocalAuthority(value, revision, generatedAt, appId) {
|
|
|
9032
9642
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
9033
9643
|
return {
|
|
9034
9644
|
...value,
|
|
9035
|
-
|
|
9645
|
+
// The author chooses the contract version; anything else defaults to 1.
|
|
9646
|
+
schemaVersion: value.schemaVersion === 2 ? 2 : 1,
|
|
9036
9647
|
revision,
|
|
9037
9648
|
generatedAt,
|
|
9038
9649
|
source: { type: "chapter", appId }
|
|
9039
9650
|
};
|
|
9040
9651
|
}
|
|
9041
9652
|
|
|
9653
|
+
// src/portal-http.ts
|
|
9654
|
+
async function readJsonObject(req, maxBytes) {
|
|
9655
|
+
const text11 = await readBoundedRequestText(req, maxBytes);
|
|
9656
|
+
if (text11 instanceof Response) return text11;
|
|
9657
|
+
try {
|
|
9658
|
+
const value = JSON.parse(text11);
|
|
9659
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
9660
|
+
} catch {
|
|
9661
|
+
}
|
|
9662
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
9663
|
+
}
|
|
9664
|
+
|
|
9665
|
+
// src/worker-routes-hub-members.ts
|
|
9666
|
+
var HUB_MEMBERS_PATH = "/api/admin/hub-members";
|
|
9667
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9668
|
+
async function grantId(emailNormalized) {
|
|
9669
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(emailNormalized));
|
|
9670
|
+
return `hub-member:${Array.from(new Uint8Array(digest).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
9671
|
+
}
|
|
9672
|
+
var view = (row2) => ({
|
|
9673
|
+
id: row2.id,
|
|
9674
|
+
email: row2.emailNormalized,
|
|
9675
|
+
firstName: row2.firstName ?? null,
|
|
9676
|
+
lastName: row2.lastName ?? null,
|
|
9677
|
+
status: row2.status,
|
|
9678
|
+
grantedBy: row2.grantedBy,
|
|
9679
|
+
grantedAt: row2.grantedAt,
|
|
9680
|
+
revokedBy: row2.revokedBy ?? null,
|
|
9681
|
+
revokedAt: row2.revokedAt ?? null,
|
|
9682
|
+
revision: row2.revision
|
|
9683
|
+
});
|
|
9684
|
+
var name = (value) => typeof value === "string" && value.trim() ? value.trim().slice(0, 80) : void 0;
|
|
9685
|
+
var handleAdminHubMembers = async (req, url, env, ctx) => {
|
|
9686
|
+
if (url.pathname !== HUB_MEMBERS_PATH && !url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) return null;
|
|
9687
|
+
if (ctx.chapter.mode !== "hub" || !servesMembers(ctx)) return json({ error: "not found" }, 404);
|
|
9688
|
+
const user = await ctx.verifyUser(req, env);
|
|
9689
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
9690
|
+
const db = ctx.makeDb(env);
|
|
9691
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
9692
|
+
const actor = user.email ?? user.userId;
|
|
9693
|
+
if (req.method === "GET" && url.pathname === HUB_MEMBERS_PATH) {
|
|
9694
|
+
const rows6 = (await db.query({ hubMemberGrants: { $: { limit: 1e3 } } })).hubMemberGrants ?? [];
|
|
9695
|
+
return json({ members: rows6.map(view).sort((a, b) => Number(b.grantedAt) - Number(a.grantedAt)) });
|
|
9696
|
+
}
|
|
9697
|
+
if (req.method === "POST" && url.pathname === HUB_MEMBERS_PATH) {
|
|
9698
|
+
const body = await readJsonObject(req, 8192);
|
|
9699
|
+
if (body instanceof Response) return body;
|
|
9700
|
+
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
9701
|
+
if (!EMAIL.test(email)) return json({ error: "a valid email is required" }, 400);
|
|
9702
|
+
const emailNormalized = normalizeChapterEmail(email);
|
|
9703
|
+
const id7 = await grantId(emailNormalized);
|
|
9704
|
+
const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
|
|
9705
|
+
if (current?.status === "active") return json({ member: view(current), status: "already-active" });
|
|
9706
|
+
const revision = Number(current?.revision ?? 0) + 1;
|
|
9707
|
+
const attrs = {
|
|
9708
|
+
id: id7,
|
|
9709
|
+
emailNormalized,
|
|
9710
|
+
status: "active",
|
|
9711
|
+
grantedBy: actor,
|
|
9712
|
+
grantedAt: Date.now(),
|
|
9713
|
+
revision,
|
|
9714
|
+
...name(body.firstName) ? { firstName: name(body.firstName) } : {},
|
|
9715
|
+
...name(body.lastName) ? { lastName: name(body.lastName) } : {}
|
|
9716
|
+
};
|
|
9717
|
+
const ops = [{ t: "update", ns: "hubMemberGrants", id: id7, attrs }];
|
|
9718
|
+
if (current) ops.push({ t: "retract", ns: "hubMemberGrants", id: id7, attrs: ["revokedBy", "revokedAt"] });
|
|
9719
|
+
try {
|
|
9720
|
+
await db.transact(ops, { mutationId: `hub-member-grant:${id7}:${revision}`, guards: [current ? { ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } } : { ns: "hubMemberGrants", id: id7, exists: false }] });
|
|
9721
|
+
} catch {
|
|
9722
|
+
return json({ error: "the roster changed; reload and try again" }, 409);
|
|
9723
|
+
}
|
|
9724
|
+
return json({ member: view({ ...current, ...attrs, revokedBy: void 0, revokedAt: void 0 }), status: current ? "re-granted" : "granted" }, current ? 200 : 201);
|
|
9725
|
+
}
|
|
9726
|
+
if (req.method === "DELETE" && url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) {
|
|
9727
|
+
const id7 = decodeURIComponent(url.pathname.slice(HUB_MEMBERS_PATH.length + 1));
|
|
9728
|
+
const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
|
|
9729
|
+
if (!current) return json({ error: "not found" }, 404);
|
|
9730
|
+
if (current.status !== "active") return json({ member: view(current), status: "already-revoked" });
|
|
9731
|
+
const attrs = { status: "revoked", revokedBy: actor, revokedAt: Date.now(), revision: Number(current.revision) + 1 };
|
|
9732
|
+
try {
|
|
9733
|
+
await db.transact([{ t: "update", ns: "hubMemberGrants", id: id7, attrs }], {
|
|
9734
|
+
mutationId: `hub-member-revoke:${id7}:${attrs.revision}`,
|
|
9735
|
+
guards: [{ ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } }]
|
|
9736
|
+
});
|
|
9737
|
+
} catch {
|
|
9738
|
+
return json({ error: "the roster changed; reload and try again" }, 409);
|
|
9739
|
+
}
|
|
9740
|
+
return json({ member: view({ ...current, ...attrs }), status: "revoked" });
|
|
9741
|
+
}
|
|
9742
|
+
return json({ error: "method not allowed" }, 405);
|
|
9743
|
+
};
|
|
9744
|
+
|
|
9745
|
+
// src/image-metadata.ts
|
|
9746
|
+
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
9747
|
+
var PNG_KEEP = /* @__PURE__ */ new Set(["IHDR", "PLTE", "IDAT", "IEND", "tRNS", "gAMA", "cHRM", "sRGB", "sBIT", "bKGD", "pHYs"]);
|
|
9748
|
+
var JPEG_KEEP = (marker) => marker === 224 || marker === 238 || marker === 219 || marker === 196 || marker === 221 || marker >= 192 && marker <= 207 && marker !== 200;
|
|
9749
|
+
var u16 = (b, i) => b[i] << 8 | b[i + 1];
|
|
9750
|
+
var u32 = (b, i) => (b[i] << 24 >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
|
|
9751
|
+
function concat(parts) {
|
|
9752
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
9753
|
+
let offset = 0;
|
|
9754
|
+
for (const part of parts) {
|
|
9755
|
+
out.set(part, offset);
|
|
9756
|
+
offset += part.length;
|
|
9757
|
+
}
|
|
9758
|
+
return out;
|
|
9759
|
+
}
|
|
9760
|
+
function cleanPng(bytes) {
|
|
9761
|
+
const parts = [bytes.subarray(0, 8)];
|
|
9762
|
+
let width = 0, height = 0, offset = 8, ended = false;
|
|
9763
|
+
while (offset + 12 <= bytes.length) {
|
|
9764
|
+
const length = u32(bytes, offset);
|
|
9765
|
+
const end = offset + 12 + length;
|
|
9766
|
+
if (end > bytes.length) return null;
|
|
9767
|
+
const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
|
|
9768
|
+
if (type === "IHDR") {
|
|
9769
|
+
width = u32(bytes, offset + 8);
|
|
9770
|
+
height = u32(bytes, offset + 12);
|
|
9771
|
+
}
|
|
9772
|
+
if (PNG_KEEP.has(type)) parts.push(bytes.subarray(offset, end));
|
|
9773
|
+
offset = end;
|
|
9774
|
+
if (type === "IEND") {
|
|
9775
|
+
ended = true;
|
|
9776
|
+
break;
|
|
9777
|
+
}
|
|
9778
|
+
}
|
|
9779
|
+
if (!ended || !width || !height) return null;
|
|
9780
|
+
return { contentType: "image/png", extension: "png", width, height, bytes: concat(parts) };
|
|
9781
|
+
}
|
|
9782
|
+
function cleanJpeg(bytes) {
|
|
9783
|
+
const parts = [bytes.subarray(0, 2)];
|
|
9784
|
+
let width = 0, height = 0, offset = 2;
|
|
9785
|
+
while (offset + 4 <= bytes.length) {
|
|
9786
|
+
if (bytes[offset] !== 255) return null;
|
|
9787
|
+
const marker = bytes[offset + 1];
|
|
9788
|
+
if (marker === 255) {
|
|
9789
|
+
offset += 1;
|
|
9790
|
+
continue;
|
|
9791
|
+
}
|
|
9792
|
+
const length = u16(bytes, offset + 2);
|
|
9793
|
+
const end = offset + 2 + length;
|
|
9794
|
+
if (length < 2 || end > bytes.length) return null;
|
|
9795
|
+
if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
|
|
9796
|
+
height = u16(bytes, offset + 5);
|
|
9797
|
+
width = u16(bytes, offset + 7);
|
|
9798
|
+
}
|
|
9799
|
+
if (marker === 218) {
|
|
9800
|
+
parts.push(bytes.subarray(offset));
|
|
9801
|
+
return width && height ? { contentType: "image/jpeg", extension: "jpg", width, height, bytes: concat(parts) } : null;
|
|
9802
|
+
}
|
|
9803
|
+
if (JPEG_KEEP(marker)) parts.push(bytes.subarray(offset, end));
|
|
9804
|
+
offset = end;
|
|
9805
|
+
}
|
|
9806
|
+
return null;
|
|
9807
|
+
}
|
|
9808
|
+
function cleanImage(bytes) {
|
|
9809
|
+
if (bytes.length > 8 && PNG_SIGNATURE.every((byte, index2) => bytes[index2] === byte)) return cleanPng(bytes);
|
|
9810
|
+
if (bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return cleanJpeg(bytes);
|
|
9811
|
+
return null;
|
|
9812
|
+
}
|
|
9813
|
+
|
|
9814
|
+
// src/member-profile.ts
|
|
9815
|
+
var MEMBER_PROFILE_FIELDS = {
|
|
9816
|
+
preferredName: "person.preferredName",
|
|
9817
|
+
location: "person.region",
|
|
9818
|
+
prompts: "person.profilePrompts",
|
|
9819
|
+
introducedByText: "person.introducedByText"
|
|
9820
|
+
};
|
|
9821
|
+
var MAX_PROMPT_ANSWER = 280;
|
|
9822
|
+
var clean = (value) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
|
|
9823
|
+
function optionalText(body, key3, max, errors) {
|
|
9824
|
+
if (!(key3 in body)) return void 0;
|
|
9825
|
+
const value = body[key3];
|
|
9826
|
+
if (value === null || value === "") return null;
|
|
9827
|
+
if (typeof value !== "string") {
|
|
9828
|
+
errors.push(`${key3} must be a string`);
|
|
9829
|
+
return void 0;
|
|
9830
|
+
}
|
|
9831
|
+
const text11 = clean(value);
|
|
9832
|
+
if (text11.length > max) errors.push(`${key3} must be at most ${max} characters`);
|
|
9833
|
+
return text11 || null;
|
|
9834
|
+
}
|
|
9835
|
+
function validateProfilePatch(body, portal) {
|
|
9836
|
+
const errors = [];
|
|
9837
|
+
for (const key3 of Object.keys(body)) {
|
|
9838
|
+
if (key3 === "expectedRevision") continue;
|
|
9839
|
+
if (!(key3 in MEMBER_PROFILE_FIELDS)) errors.push(`${key3} is not an editable profile field`);
|
|
9840
|
+
else if (!MEMBER_SELF_EDITABLE_FIELDS.has(MEMBER_PROFILE_FIELDS[key3])) errors.push(`${key3} is read-only`);
|
|
9841
|
+
}
|
|
9842
|
+
const patch = {};
|
|
9843
|
+
const preferredName = optionalText(body, "preferredName", 80, errors);
|
|
9844
|
+
if (preferredName !== void 0) patch.preferredName = preferredName;
|
|
9845
|
+
const location = optionalText(body, "location", 80, errors);
|
|
9846
|
+
if (location !== void 0) patch.location = location;
|
|
9847
|
+
const introducedByText = optionalText(body, "introducedByText", 120, errors);
|
|
9848
|
+
if (introducedByText !== void 0) patch.introducedByText = introducedByText;
|
|
9849
|
+
if ("prompts" in body) {
|
|
9850
|
+
const raw2 = body.prompts;
|
|
9851
|
+
if (!Array.isArray(raw2)) errors.push("prompts must be an array");
|
|
9852
|
+
else {
|
|
9853
|
+
if (raw2.length > portal.maxPrompts) errors.push(`prompts supports at most ${portal.maxPrompts} answers`);
|
|
9854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9855
|
+
const prompts = [];
|
|
9856
|
+
for (const [index2, item] of raw2.entries()) {
|
|
9857
|
+
const entry = item && typeof item === "object" ? item : {};
|
|
9858
|
+
const prompt = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
|
|
9859
|
+
const answer = typeof entry.answer === "string" ? clean(entry.answer) : "";
|
|
9860
|
+
if (!portal.profilePrompts.includes(prompt)) errors.push(`prompts[${index2}].prompt is not one of this site's prompts`);
|
|
9861
|
+
if (seen.has(prompt)) errors.push(`prompts[${index2}].prompt is already answered`);
|
|
9862
|
+
if (entry.answer !== void 0 && typeof entry.answer !== "string") errors.push(`prompts[${index2}].answer must be a string`);
|
|
9863
|
+
if (answer.length > MAX_PROMPT_ANSWER) errors.push(`prompts[${index2}].answer must be at most ${MAX_PROMPT_ANSWER} characters`);
|
|
9864
|
+
seen.add(prompt);
|
|
9865
|
+
prompts.push({ prompt, answer });
|
|
9866
|
+
}
|
|
9867
|
+
patch.prompts = prompts;
|
|
9868
|
+
}
|
|
9869
|
+
}
|
|
9870
|
+
return errors.length ? { ok: false, errors } : { ok: true, patch };
|
|
9871
|
+
}
|
|
9872
|
+
var nameKey = (value) => value.toLowerCase().normalize("NFKC").replace(/[^\p{L}\p{N}\s'-]/gu, " ").replace(/\s+/g, " ").trim();
|
|
9873
|
+
|
|
9874
|
+
// src/member-profile-store.ts
|
|
9875
|
+
async function loadMemberProfile(db, memberId) {
|
|
9876
|
+
return (await db.query({ memberProfiles: { $: { where: { id: memberId }, limit: 1 } } })).memberProfiles?.[0] ?? null;
|
|
9877
|
+
}
|
|
9878
|
+
async function membersNamed(db, mode, chapterId, text11, selfId) {
|
|
9879
|
+
const wanted = nameKey(text11);
|
|
9880
|
+
if (!wanted) return [];
|
|
9881
|
+
const rows6 = mode === "hub" ? (await db.query({ hubMemberGrants: { $: { where: { status: "active" }, limit: 5e3 } } })).hubMemberGrants ?? [] : ((await db.query({ applications: { $: { where: { groupId: chapterId, status: "approved" }, limit: 5e3 } } })).applications ?? []).filter((row2) => row2.canceled !== true);
|
|
9882
|
+
return rows6.filter((row2) => String(row2.id) !== selfId && nameKey(`${row2.firstName ?? ""} ${row2.lastName ?? ""}`) === wanted).map((row2) => String(row2.id));
|
|
9883
|
+
}
|
|
9884
|
+
function reviewItemOp(id7, kind, detail, memberId, relatedMemberId) {
|
|
9885
|
+
return { t: "update", ns: "portalReviewItems", id: id7, attrs: {
|
|
9886
|
+
id: id7,
|
|
9887
|
+
kind,
|
|
9888
|
+
detail,
|
|
9889
|
+
status: "open",
|
|
9890
|
+
createdAt: Date.now(),
|
|
9891
|
+
...memberId ? { memberId } : {},
|
|
9892
|
+
...relatedMemberId ? { relatedMemberId } : {}
|
|
9893
|
+
} };
|
|
9894
|
+
}
|
|
9895
|
+
async function saveMemberProfile(db, input) {
|
|
9896
|
+
const current = await loadMemberProfile(db, input.memberId);
|
|
9897
|
+
if (Number(current?.revision ?? 0) !== input.expectedRevision) throw new Error("member_profile_revision_conflict");
|
|
9898
|
+
const now = Date.now();
|
|
9899
|
+
const revision = input.expectedRevision + 1;
|
|
9900
|
+
const attrs = { id: input.memberId, memberId: input.memberId, revision, updatedAt: now };
|
|
9901
|
+
const retract = [];
|
|
9902
|
+
const set = (key3, value) => {
|
|
9903
|
+
if (value === null) retract.push(key3);
|
|
9904
|
+
else attrs[key3] = value;
|
|
9905
|
+
};
|
|
9906
|
+
if (input.patch.preferredName !== void 0) set("preferredName", input.patch.preferredName);
|
|
9907
|
+
if (input.patch.location !== void 0) set("location", input.patch.location);
|
|
9908
|
+
if (input.patch.prompts !== void 0) attrs.prompts = input.patch.prompts;
|
|
9909
|
+
const ops = [];
|
|
9910
|
+
let review = null;
|
|
9911
|
+
const introduced = input.patch.introducedByText;
|
|
9912
|
+
if (introduced !== void 0 && introduced !== (current?.introducedByText ?? null)) {
|
|
9913
|
+
set("introducedByText", introduced);
|
|
9914
|
+
if (introduced === null) retract.push("introducedByMemberId", "introducedAt");
|
|
9915
|
+
else {
|
|
9916
|
+
const matches2 = await membersNamed(db, input.mode, input.chapterId, introduced, input.memberId);
|
|
9917
|
+
if (matches2.length === 1) {
|
|
9918
|
+
Object.assign(attrs, { introducedByMemberId: matches2[0], introducedAt: now });
|
|
9919
|
+
review = "matched";
|
|
9920
|
+
} else {
|
|
9921
|
+
retract.push("introducedByMemberId", "introducedAt");
|
|
9922
|
+
ops.push(reviewItemOp(
|
|
9923
|
+
`introduction:${input.memberId}:${revision}`,
|
|
9924
|
+
"introduction_unmatched",
|
|
9925
|
+
{ text: introduced, candidates: matches2.length },
|
|
9926
|
+
input.memberId
|
|
9927
|
+
));
|
|
9928
|
+
review = "queued";
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
ops.unshift({ t: "update", ns: "memberProfiles", id: input.memberId, attrs });
|
|
9933
|
+
const absent = retract.filter((key3) => current && current[key3] !== void 0);
|
|
9934
|
+
if (absent.length) ops.splice(1, 0, { t: "retract", ns: "memberProfiles", id: input.memberId, attrs: absent });
|
|
9935
|
+
await db.transact(ops, {
|
|
9936
|
+
mutationId: `member-profile:${input.memberId}:${revision}`,
|
|
9937
|
+
guards: [current ? { ns: "memberProfiles", id: input.memberId, exists: true, equals: { revision: input.expectedRevision } } : { ns: "memberProfiles", id: input.memberId, exists: false }]
|
|
9938
|
+
});
|
|
9939
|
+
const row2 = { ...current, ...attrs };
|
|
9940
|
+
for (const key3 of absent) delete row2[key3];
|
|
9941
|
+
return { row: row2, review };
|
|
9942
|
+
}
|
|
9943
|
+
|
|
9944
|
+
// src/worker-routes-member-profile.ts
|
|
9945
|
+
var MEMBER_PROFILE_PATH = "/api/member/profile";
|
|
9946
|
+
var MEMBER_PHOTO_PATH = "/api/member/profile/photo";
|
|
9947
|
+
var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
|
|
9948
|
+
var PHOTO_SIZE = 400;
|
|
9949
|
+
async function portalMemberGate(req, env, ctx) {
|
|
9950
|
+
const user = await ctx.verifyUser(req, env);
|
|
9951
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
9952
|
+
const db = ctx.makeDb(env);
|
|
9953
|
+
const resolved = await resolvePortalMember(db, env, ctx, user);
|
|
9954
|
+
if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
|
|
9955
|
+
if (resolved.status !== "member") return json({ error: "member access required" }, 403);
|
|
9956
|
+
return { db, member: resolved.member };
|
|
9957
|
+
}
|
|
9958
|
+
async function profileView(db, ctx, member) {
|
|
9959
|
+
const row2 = await loadMemberProfile(db, member.memberId);
|
|
9960
|
+
const portal = ctx.chapter.memberPortal;
|
|
9961
|
+
return {
|
|
9962
|
+
revision: Number(row2?.revision ?? 0),
|
|
9963
|
+
identity: await portalIdentity(db, ctx, member),
|
|
9964
|
+
profile: {
|
|
9965
|
+
preferredName: row2?.preferredName ?? null,
|
|
9966
|
+
location: row2?.location ?? null,
|
|
9967
|
+
prompts: Array.isArray(row2?.prompts) ? row2.prompts : [],
|
|
9968
|
+
introducedByText: row2?.introducedByText ?? null,
|
|
9969
|
+
introducedByMatched: typeof row2?.introducedByMemberId === "string"
|
|
9970
|
+
},
|
|
9971
|
+
promptChoices: portal.profilePrompts,
|
|
9972
|
+
maxPrompts: portal.maxPrompts,
|
|
9973
|
+
photos: portal.photos
|
|
9974
|
+
};
|
|
9975
|
+
}
|
|
9976
|
+
var handleMemberProfile = async (req, url, env, ctx) => {
|
|
9977
|
+
if (url.pathname !== MEMBER_PROFILE_PATH && url.pathname !== MEMBER_PHOTO_PATH) return null;
|
|
9978
|
+
const portal = ctx.chapter.memberPortal;
|
|
9979
|
+
if (!portal) return json({ error: "not found" }, 404);
|
|
9980
|
+
const gate12 = await portalMemberGate(req, env, ctx);
|
|
9981
|
+
if (gate12 instanceof Response) return gate12;
|
|
9982
|
+
const { db, member } = gate12;
|
|
9983
|
+
if (url.pathname === MEMBER_PHOTO_PATH) return uploadPhoto(req, db, ctx, member);
|
|
9984
|
+
if (req.method === "GET") return json(await profileView(db, ctx, member));
|
|
9985
|
+
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
9986
|
+
const body = await readJsonObject(req, 16384);
|
|
9987
|
+
if (body instanceof Response) return body;
|
|
9988
|
+
const expectedRevision = body.expectedRevision;
|
|
9989
|
+
if (!Number.isInteger(expectedRevision) || Number(expectedRevision) < 0) {
|
|
9990
|
+
return json({ error: "expectedRevision must be a non-negative integer" }, 400);
|
|
9991
|
+
}
|
|
9992
|
+
const checked = validateProfilePatch(body, portal);
|
|
9993
|
+
if (!checked.ok) return json({ error: "invalid profile", errors: checked.errors }, 400);
|
|
9994
|
+
try {
|
|
9995
|
+
const saved = await saveMemberProfile(db, {
|
|
9996
|
+
mode: ctx.chapter.mode,
|
|
9997
|
+
chapterId: ctx.chapter.id,
|
|
9998
|
+
memberId: member.memberId,
|
|
9999
|
+
patch: checked.patch,
|
|
10000
|
+
expectedRevision: Number(expectedRevision)
|
|
10001
|
+
});
|
|
10002
|
+
return json({ ...await profileView(db, ctx, member), introducedBy: saved.review });
|
|
10003
|
+
} catch (error) {
|
|
10004
|
+
if (error instanceof Error && error.message === "member_profile_revision_conflict") {
|
|
10005
|
+
return json({ error: "your profile changed elsewhere; reload before saving" }, 409);
|
|
10006
|
+
}
|
|
10007
|
+
return json({ error: "profile could not be saved" }, 503);
|
|
10008
|
+
}
|
|
10009
|
+
};
|
|
10010
|
+
async function uploadPhoto(req, db, ctx, member) {
|
|
10011
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
10012
|
+
if (!ctx.chapter.memberPortal?.photos || !db.storage) return json({ error: "photos are not enabled" }, 404);
|
|
10013
|
+
const declared = Number(req.headers.get("content-length") ?? 0);
|
|
10014
|
+
if (declared > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
|
|
10015
|
+
const bytes = new Uint8Array(await req.arrayBuffer());
|
|
10016
|
+
if (bytes.length > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
|
|
10017
|
+
const image = cleanImage(bytes);
|
|
10018
|
+
if (!image) return json({ error: "photo must be a JPEG or PNG" }, 415);
|
|
10019
|
+
if (image.width > PHOTO_SIZE || image.height > PHOTO_SIZE) {
|
|
10020
|
+
return json({ error: `photo must be at most ${PHOTO_SIZE} by ${PHOTO_SIZE} pixels` }, 422);
|
|
10021
|
+
}
|
|
10022
|
+
const now = Date.now();
|
|
10023
|
+
const path = `member-photos/${encodeURIComponent(member.memberId)}/${now}.${image.extension}`;
|
|
10024
|
+
await db.storage.upload(path, new Blob([image.bytes], { type: image.contentType }), image.contentType, { private: true });
|
|
10025
|
+
const previous = await loadMemberProfile(db, member.memberId);
|
|
10026
|
+
await db.transact([{ t: "update", ns: "memberProfiles", id: member.memberId, attrs: {
|
|
10027
|
+
id: member.memberId,
|
|
10028
|
+
memberId: member.memberId,
|
|
10029
|
+
photoPath: path,
|
|
10030
|
+
photoUpdatedAt: now,
|
|
10031
|
+
updatedAt: now,
|
|
10032
|
+
...previous ? {} : { revision: 0 }
|
|
10033
|
+
} }], { mutationId: `member-photo:${member.memberId}:${now}` });
|
|
10034
|
+
const old = typeof previous?.photoPath === "string" ? previous.photoPath : null;
|
|
10035
|
+
if (old && old !== path) await db.storage.delete(old).catch(() => void 0);
|
|
10036
|
+
return json({ photoUrl: await db.storage.sign(path, 3600), width: image.width, height: image.height });
|
|
10037
|
+
}
|
|
10038
|
+
|
|
10039
|
+
// src/worker-routes-admin-member-tier.ts
|
|
10040
|
+
var PATH5 = /^\/api\/admin\/applications\/([^/]+)\/entitlement-tier$/;
|
|
10041
|
+
var handleAdminMemberTier = async (req, url, env, ctx) => {
|
|
10042
|
+
const match = PATH5.exec(url.pathname);
|
|
10043
|
+
if (!match) return null;
|
|
10044
|
+
if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
|
|
10045
|
+
if (!["GET", "POST", "DELETE"].includes(req.method)) return json({ error: "method not allowed" }, 405);
|
|
10046
|
+
const user = await ctx.verifyUser(req, env);
|
|
10047
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10048
|
+
const db = ctx.makeDb(env);
|
|
10049
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10050
|
+
const id7 = decodeURIComponent(match[1]);
|
|
10051
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
10052
|
+
if (!app || app.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
10053
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
10054
|
+
const tiers = group ? await loadTiers(db, group) : [];
|
|
10055
|
+
if (req.method === "GET") {
|
|
10056
|
+
const history = (await db.query({ memberTierChanges: { $: { where: { applicationId: id7 }, limit: 50 } } })).memberTierChanges ?? [];
|
|
10057
|
+
return json({
|
|
10058
|
+
purchasedTierId: app.tierId ?? null,
|
|
10059
|
+
entitledTierId: entitledTierId(app),
|
|
10060
|
+
entitlementTierEndsAt: app.entitlementTierEndsAt ?? null,
|
|
10061
|
+
tiers: tiers.filter((tier) => tier.active !== false).map(({ id: tierId, name: name2 }) => ({ id: tierId, name: name2 })),
|
|
10062
|
+
history: history.sort((a, b) => Number(b.at) - Number(a.at))
|
|
10063
|
+
});
|
|
10064
|
+
}
|
|
10065
|
+
const body = await readJsonObject(req, 8192);
|
|
10066
|
+
if (body instanceof Response) return body;
|
|
10067
|
+
if (app.status !== "approved" || app.canceled === true) return json({ error: "only an approved member can change tier" }, 409);
|
|
10068
|
+
const reason = typeof body.reason === "string" ? body.reason.trim() : "";
|
|
10069
|
+
if (!reason || reason.length > 500) return json({ error: "a reason of up to 500 characters is required" }, 400);
|
|
10070
|
+
const paymentReference = typeof body.paymentReference === "string" && body.paymentReference.trim() ? body.paymentReference.trim().slice(0, 200) : void 0;
|
|
10071
|
+
const now = Date.now();
|
|
10072
|
+
let toTierId;
|
|
10073
|
+
let endsAt;
|
|
10074
|
+
if (req.method === "POST") {
|
|
10075
|
+
toTierId = typeof body.tierId === "string" ? body.tierId : "";
|
|
10076
|
+
const tier = tiers.find((candidate) => candidate.id === toTierId);
|
|
10077
|
+
if (!tier || tier.active === false) return json({ error: "tierId must be an active tier" }, 400);
|
|
10078
|
+
if (body.endsAt !== void 0) {
|
|
10079
|
+
endsAt = Number(body.endsAt);
|
|
10080
|
+
if (!Number.isSafeInteger(endsAt) || endsAt <= now) return json({ error: "endsAt must be a future timestamp" }, 400);
|
|
10081
|
+
}
|
|
10082
|
+
} else if (app.entitlementTierId === void 0) return json({ error: "no entitlement tier to revert" }, 409);
|
|
10083
|
+
const fromTierId = entitledTierId(app, now) ?? void 0;
|
|
10084
|
+
const changeId = `tier-change:${id7}:${now}`;
|
|
10085
|
+
const ops = [
|
|
10086
|
+
{ t: "update", ns: "memberTierChanges", id: changeId, attrs: {
|
|
10087
|
+
id: changeId,
|
|
10088
|
+
applicationId: id7,
|
|
10089
|
+
reason,
|
|
10090
|
+
actor: user.email ?? user.userId,
|
|
10091
|
+
at: now,
|
|
10092
|
+
...fromTierId ? { fromTierId } : {},
|
|
10093
|
+
...toTierId ? { toTierId } : {},
|
|
10094
|
+
...endsAt ? { endsAt } : {},
|
|
10095
|
+
...paymentReference ? { paymentReference } : {}
|
|
10096
|
+
} },
|
|
10097
|
+
toTierId ? { t: "update", ns: "applications", id: id7, attrs: { entitlementTierId: toTierId, ...endsAt ? { entitlementTierEndsAt: endsAt } : {} } } : { t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierId", "entitlementTierEndsAt"] }
|
|
10098
|
+
];
|
|
10099
|
+
if (toTierId && !endsAt && app.entitlementTierEndsAt !== void 0) ops.push({ t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierEndsAt"] });
|
|
10100
|
+
const current = app.entitlementTierId;
|
|
10101
|
+
try {
|
|
10102
|
+
await db.transact(ops, { mutationId: changeId, guards: [current === void 0 ? { ns: "applications", id: id7, exists: true, equals: { status: "approved" }, absent: ["entitlementTierId"] } : { ns: "applications", id: id7, exists: true, equals: { status: "approved", entitlementTierId: current } }] });
|
|
10103
|
+
} catch {
|
|
10104
|
+
return json({ error: "this member changed; reload and try again" }, 409);
|
|
10105
|
+
}
|
|
10106
|
+
return json({ ok: true, applicationId: id7, fromTierId: fromTierId ?? null, toTierId: toTierId ?? (typeof app.tierId === "string" ? app.tierId : null), endsAt: endsAt ?? null });
|
|
10107
|
+
};
|
|
10108
|
+
|
|
10109
|
+
// src/worker-routes-admin-portal-review.ts
|
|
10110
|
+
var PORTAL_REVIEW_PATH = "/api/admin/portal-review";
|
|
10111
|
+
var handleAdminPortalReview = async (req, url, env, ctx) => {
|
|
10112
|
+
if (url.pathname !== PORTAL_REVIEW_PATH && !url.pathname.startsWith(`${PORTAL_REVIEW_PATH}/`)) return null;
|
|
10113
|
+
if (!ctx.chapter.memberPortal) return json({ error: "not found" }, 404);
|
|
10114
|
+
const user = await ctx.verifyUser(req, env);
|
|
10115
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10116
|
+
const db = ctx.makeDb(env);
|
|
10117
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10118
|
+
if (req.method === "GET" && url.pathname === PORTAL_REVIEW_PATH) {
|
|
10119
|
+
const status2 = url.searchParams.get("status") ?? "open";
|
|
10120
|
+
const where = status2 === "all" ? {} : { status: status2 };
|
|
10121
|
+
const items = (await db.query({ portalReviewItems: { $: { where, limit: 1e3 } } })).portalReviewItems ?? [];
|
|
10122
|
+
return json({ items: items.sort((a, b) => Number(b.createdAt) - Number(a.createdAt)) });
|
|
10123
|
+
}
|
|
10124
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
10125
|
+
const id7 = decodeURIComponent(url.pathname.slice(PORTAL_REVIEW_PATH.length + 1));
|
|
10126
|
+
const body = await readJsonObject(req, 4096);
|
|
10127
|
+
if (body instanceof Response) return body;
|
|
10128
|
+
const decision = typeof body.decision === "string" ? body.decision.trim() : "";
|
|
10129
|
+
if (!decision || decision.length > 1e3) return json({ error: "a decision of up to 1,000 characters is required" }, 400);
|
|
10130
|
+
const item = (await db.query({ portalReviewItems: { $: { where: { id: id7 }, limit: 1 } } })).portalReviewItems?.[0];
|
|
10131
|
+
if (!item) return json({ error: "not found" }, 404);
|
|
10132
|
+
if (item.status !== "open") return json({ error: "already resolved" }, 409);
|
|
10133
|
+
const attrs = { status: "resolved", decision, resolvedBy: user.email ?? user.userId, resolvedAt: Date.now() };
|
|
10134
|
+
try {
|
|
10135
|
+
await db.transact([{ t: "update", ns: "portalReviewItems", id: id7, attrs }], {
|
|
10136
|
+
mutationId: `portal-review:${id7}:resolve`,
|
|
10137
|
+
guards: [{ ns: "portalReviewItems", id: id7, exists: true, equals: { status: "open" } }]
|
|
10138
|
+
});
|
|
10139
|
+
} catch {
|
|
10140
|
+
return json({ error: "already resolved" }, 409);
|
|
10141
|
+
}
|
|
10142
|
+
return json({ item: { ...item, ...attrs } });
|
|
10143
|
+
};
|
|
10144
|
+
|
|
10145
|
+
// src/worker-routes-member-portal.ts
|
|
10146
|
+
var PORTAL_ROUTES = [
|
|
10147
|
+
handleMemberProfile,
|
|
10148
|
+
handleAdminHubMembers,
|
|
10149
|
+
handleAdminMemberTier,
|
|
10150
|
+
handleAdminPortalReview
|
|
10151
|
+
];
|
|
10152
|
+
var handleMemberPortal = async (req, url, env, ctx) => {
|
|
10153
|
+
for (const route of PORTAL_ROUTES) {
|
|
10154
|
+
const response = await route(req, url, env, ctx);
|
|
10155
|
+
if (response) return response;
|
|
10156
|
+
}
|
|
10157
|
+
return null;
|
|
10158
|
+
};
|
|
10159
|
+
|
|
9042
10160
|
// src/worker-routes-admission-grants.ts
|
|
9043
10161
|
import { verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
|
|
9044
10162
|
var reject5 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
|
|
@@ -9419,9 +10537,9 @@ function integer3(value, fallback, min, max) {
|
|
|
9419
10537
|
const parsed = Number(value);
|
|
9420
10538
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
9421
10539
|
}
|
|
9422
|
-
function projectRecord(record4,
|
|
10540
|
+
function projectRecord(record4, allowed, editable, stagesEnabled, crm) {
|
|
9423
10541
|
const fields = Object.fromEntries(
|
|
9424
|
-
|
|
10542
|
+
allowed.flatMap((name2) => Object.prototype.hasOwnProperty.call(record4.fields, name2) ? [[name2, record4.fields[name2]]] : [])
|
|
9425
10543
|
);
|
|
9426
10544
|
const def = crm.type(record4.type);
|
|
9427
10545
|
const editFields = editable.map((id7) => {
|
|
@@ -9531,7 +10649,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
9531
10649
|
}
|
|
9532
10650
|
|
|
9533
10651
|
// src/worker-routes-network-admission.ts
|
|
9534
|
-
var
|
|
10652
|
+
var PATH6 = "/api/admin/network/admissions";
|
|
9535
10653
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
9536
10654
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
9537
10655
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -9611,11 +10729,11 @@ async function parseBody(req) {
|
|
|
9611
10729
|
}
|
|
9612
10730
|
}
|
|
9613
10731
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
9614
|
-
if (![
|
|
10732
|
+
if (![PATH6, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
9615
10733
|
const auth = await gate2(req, env, ctx);
|
|
9616
10734
|
if ("response" in auth) return auth.response;
|
|
9617
10735
|
const db = auth.db;
|
|
9618
|
-
if (url.pathname ===
|
|
10736
|
+
if (url.pathname === PATH6 && req.method === "GET") {
|
|
9619
10737
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
9620
10738
|
const target2 = targetFor(ctx, targetId2);
|
|
9621
10739
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -9763,8 +10881,8 @@ function makeCalendar(env) {
|
|
|
9763
10881
|
}
|
|
9764
10882
|
async function firstRow3(db, ns, q) {
|
|
9765
10883
|
const res = await db.query({ [ns]: { $: q } });
|
|
9766
|
-
const
|
|
9767
|
-
return Array.isArray(
|
|
10884
|
+
const rows6 = res[ns];
|
|
10885
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
9768
10886
|
}
|
|
9769
10887
|
async function computeSlots(cal, cfg) {
|
|
9770
10888
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -10623,18 +11741,18 @@ async function startSubscription(req, env, ctx) {
|
|
|
10623
11741
|
|
|
10624
11742
|
// src/worker-payment-receipt.ts
|
|
10625
11743
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10626
|
-
var escape = (
|
|
11744
|
+
var escape = (text11) => text11.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10627
11745
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10628
11746
|
const id7 = url.searchParams.get("application");
|
|
10629
11747
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
10630
11748
|
if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
|
|
10631
11749
|
const db = ctx.makeDb(env);
|
|
10632
|
-
const
|
|
10633
|
-
const app =
|
|
11750
|
+
const rows6 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11751
|
+
const app = rows6.applications?.[0], intent = rows6.subscriptionCheckoutIntents?.[0];
|
|
10634
11752
|
if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
|
|
10635
11753
|
const receipt = intent.paymentReceipt;
|
|
10636
11754
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10637
|
-
const
|
|
11755
|
+
const text11 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10638
11756
|
if (req.method === "POST") {
|
|
10639
11757
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10640
11758
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10652,7 +11770,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10652
11770
|
template: "paymentReceipt",
|
|
10653
11771
|
to: String(intent.purchaserEmailNormalized),
|
|
10654
11772
|
applicationId: id7,
|
|
10655
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
11773
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text11 },
|
|
10656
11774
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10657
11775
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10658
11776
|
});
|
|
@@ -10662,7 +11780,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10662
11780
|
const nonce = crypto.randomUUID();
|
|
10663
11781
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10664
11782
|
<style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
|
|
10665
|
-
<h1>Payment receipt</h1><pre>${escape(
|
|
11783
|
+
<h1>Payment receipt</h1><pre>${escape(text11)}</pre><button id="print">Print / save PDF</button>
|
|
10666
11784
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10667
11785
|
headers: {
|
|
10668
11786
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10695,7 +11813,7 @@ var handlePayments = async (req, url, env, ctx) => {
|
|
|
10695
11813
|
|
|
10696
11814
|
// src/worker-routes-gifts.ts
|
|
10697
11815
|
var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
|
|
10698
|
-
var
|
|
11816
|
+
var rows4 = (value) => Array.isArray(value) ? value : [];
|
|
10699
11817
|
var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
|
|
10700
11818
|
var userFor = async (req, env, ctx) => {
|
|
10701
11819
|
const user = await ctx.verifyUser(req, env);
|
|
@@ -10733,7 +11851,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
10733
11851
|
const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
10734
11852
|
return json({
|
|
10735
11853
|
offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
|
|
10736
|
-
claims: buyerGiftClaims(
|
|
11854
|
+
claims: buyerGiftClaims(rows4(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
|
|
10737
11855
|
refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
|
|
10738
11856
|
merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
|
|
10739
11857
|
});
|
|
@@ -10756,7 +11874,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
10756
11874
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
10757
11875
|
if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
|
|
10758
11876
|
const emailNormalized = normalizeChapterEmail(user.email);
|
|
10759
|
-
const applications =
|
|
11877
|
+
const applications = rows4((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
|
|
10760
11878
|
const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
10761
11879
|
try {
|
|
10762
11880
|
return json(await startGiftIntent(db, {
|
|
@@ -10886,9 +12004,9 @@ var receiptFor2 = (seat, released) => ({
|
|
|
10886
12004
|
});
|
|
10887
12005
|
async function regiftNamedSeat(db, input, now) {
|
|
10888
12006
|
membershipClock(now);
|
|
10889
|
-
const
|
|
12007
|
+
const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
10890
12008
|
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
10891
|
-
if (
|
|
12009
|
+
if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
|
|
10892
12010
|
const state = await loadMembershipAuthorityState(db, input);
|
|
10893
12011
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
10894
12012
|
requireMembershipScope(seat, input);
|
|
@@ -10897,7 +12015,7 @@ async function regiftNamedSeat(db, input, now) {
|
|
|
10897
12015
|
if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
|
|
10898
12016
|
if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
|
|
10899
12017
|
const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
|
|
10900
|
-
if (seat.recipientEmailNormalized === email && seat.recipientName ===
|
|
12018
|
+
if (seat.recipientEmailNormalized === email && seat.recipientName === name2 && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
|
|
10901
12019
|
return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
|
|
10902
12020
|
}
|
|
10903
12021
|
if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
|
|
@@ -10935,7 +12053,7 @@ async function regiftNamedSeat(db, input, now) {
|
|
|
10935
12053
|
});
|
|
10936
12054
|
const ops = [
|
|
10937
12055
|
membershipUpdate("networkSeat", String(seat.id), {
|
|
10938
|
-
recipientName:
|
|
12056
|
+
recipientName: name2,
|
|
10939
12057
|
recipientEmailNormalized: email,
|
|
10940
12058
|
recipientClaimId: claimId,
|
|
10941
12059
|
status: "pending_acceptance",
|
|
@@ -11132,16 +12250,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
11132
12250
|
};
|
|
11133
12251
|
|
|
11134
12252
|
// src/named-seat-accepted-notice.ts
|
|
11135
|
-
var
|
|
12253
|
+
var text8 = (value) => typeof value === "string" ? value : "";
|
|
11136
12254
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
11137
12255
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
11138
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
12256
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text8(purchaser.email)) {
|
|
11139
12257
|
return { sent: false, reason: "purchaser-missing" };
|
|
11140
12258
|
}
|
|
11141
12259
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
11142
12260
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
11143
12261
|
const config = emailGroupFrom(group);
|
|
11144
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
12262
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text8).join(" ").replace(/\s+/g, " ").trim();
|
|
11145
12263
|
return sendTemplated({
|
|
11146
12264
|
db,
|
|
11147
12265
|
envName: env.ODLA_ENV,
|
|
@@ -11153,12 +12271,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
11153
12271
|
}, {
|
|
11154
12272
|
group: config,
|
|
11155
12273
|
template: "seatAccepted",
|
|
11156
|
-
to:
|
|
12274
|
+
to: text8(purchaser.email),
|
|
11157
12275
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
11158
12276
|
applicationId: input.primaryApplicationId,
|
|
11159
12277
|
tierId: input.skuId,
|
|
11160
12278
|
vars: {
|
|
11161
|
-
firstName:
|
|
12279
|
+
firstName: text8(purchaser.firstName),
|
|
11162
12280
|
purchaserName,
|
|
11163
12281
|
recipientName: input.recipientName,
|
|
11164
12282
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -11619,8 +12737,8 @@ async function upcomingEvents(env) {
|
|
|
11619
12737
|
const res = await cal.availability.upcoming();
|
|
11620
12738
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
11621
12739
|
}
|
|
11622
|
-
function toReconcile(
|
|
11623
|
-
return
|
|
12740
|
+
function toReconcile(rows6) {
|
|
12741
|
+
return rows6.map((m) => ({
|
|
11624
12742
|
id: String(m.id),
|
|
11625
12743
|
applicationId: String(m.applicationId),
|
|
11626
12744
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -11642,12 +12760,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
11642
12760
|
const query = await db.query({
|
|
11643
12761
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
11644
12762
|
});
|
|
11645
|
-
let
|
|
11646
|
-
if (Number.isFinite(from))
|
|
11647
|
-
if (Number.isFinite(to))
|
|
12763
|
+
let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
12764
|
+
if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
12765
|
+
if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
11648
12766
|
let decisions = [];
|
|
11649
12767
|
try {
|
|
11650
|
-
decisions = reconcileMeetings(toReconcile(
|
|
12768
|
+
decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
|
|
11651
12769
|
} catch {
|
|
11652
12770
|
}
|
|
11653
12771
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -11674,7 +12792,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
11674
12792
|
};
|
|
11675
12793
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
11676
12794
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
11677
|
-
const meetings =
|
|
12795
|
+
const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
11678
12796
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
11679
12797
|
};
|
|
11680
12798
|
|
|
@@ -11813,15 +12931,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
11813
12931
|
for (const a of data.applications ?? []) {
|
|
11814
12932
|
const key3 = String(a.email ?? "").toLowerCase();
|
|
11815
12933
|
if (!key3) continue;
|
|
11816
|
-
const
|
|
12934
|
+
const name2 = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
|
|
11817
12935
|
const row2 = people.get(key3);
|
|
11818
12936
|
if (row2) {
|
|
11819
12937
|
if (!row2.application) row2.application = applicationSummary(a);
|
|
11820
|
-
if (!row2.name) row2.name =
|
|
12938
|
+
if (!row2.name) row2.name = name2;
|
|
11821
12939
|
} else {
|
|
11822
12940
|
people.set(key3, {
|
|
11823
12941
|
email: String(a.email),
|
|
11824
|
-
name,
|
|
12942
|
+
name: name2,
|
|
11825
12943
|
userId: null,
|
|
11826
12944
|
role: null,
|
|
11827
12945
|
profile: null,
|
|
@@ -11830,10 +12948,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
11830
12948
|
});
|
|
11831
12949
|
}
|
|
11832
12950
|
}
|
|
11833
|
-
const
|
|
12951
|
+
const rows6 = [...people.values()].sort(
|
|
11834
12952
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
11835
12953
|
);
|
|
11836
|
-
return json({ people:
|
|
12954
|
+
return json({ people: rows6 });
|
|
11837
12955
|
};
|
|
11838
12956
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
11839
12957
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
@@ -12034,7 +13152,7 @@ async function gate3(req, env, ctx) {
|
|
|
12034
13152
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
12035
13153
|
return rawDb;
|
|
12036
13154
|
}
|
|
12037
|
-
var
|
|
13155
|
+
var rows5 = (v) => Array.isArray(v) ? v : [];
|
|
12038
13156
|
var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
12039
13157
|
if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
|
|
12040
13158
|
const got = await gate3(req, env, ctx);
|
|
@@ -12056,8 +13174,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12056
13174
|
crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
|
|
12057
13175
|
groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
|
|
12058
13176
|
});
|
|
12059
|
-
const apps =
|
|
12060
|
-
const incomplete =
|
|
13177
|
+
const apps = rows5(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
|
|
13178
|
+
const incomplete = rows5(data.applications).length >= 1e3 || rows5(data.crm_record).length >= 1e3 || rows5(data.meetings).length >= 500;
|
|
12061
13179
|
const applications = {
|
|
12062
13180
|
total: apps.length,
|
|
12063
13181
|
last7: apps.filter((a) => a.createdAt >= d7).length,
|
|
@@ -12073,7 +13191,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12073
13191
|
for (const a of apps) {
|
|
12074
13192
|
if (String(a.status) in pipeline) pipeline[String(a.status)] = (pipeline[String(a.status)] ?? 0) + 1;
|
|
12075
13193
|
}
|
|
12076
|
-
for (const r of
|
|
13194
|
+
for (const r of rows5(data.crm_record)) {
|
|
12077
13195
|
if (!appById.has(r.fields?.applicationId)) continue;
|
|
12078
13196
|
const s = r.stage;
|
|
12079
13197
|
if (s in pipeline) {
|
|
@@ -12081,7 +13199,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12081
13199
|
if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
|
|
12082
13200
|
}
|
|
12083
13201
|
}
|
|
12084
|
-
const meetings =
|
|
13202
|
+
const meetings = rows5(data.meetings).filter((m) => appById.has(m.applicationId));
|
|
12085
13203
|
const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
|
|
12086
13204
|
const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
|
|
12087
13205
|
const agenda = upcoming.slice(0, 8).map((m) => {
|
|
@@ -12104,12 +13222,12 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12104
13222
|
const sk = await getVaultSecret(db, "stripe_secret_key");
|
|
12105
13223
|
if (sk) {
|
|
12106
13224
|
const subsRes = await chapterBillingSubscriptions(sk, ctx.chapter.id, env).catch(() => null);
|
|
12107
|
-
if (subsRes?.complete &&
|
|
13225
|
+
if (subsRes?.complete && rows5(data.applications).length < 1e3) {
|
|
12108
13226
|
const subs = subsRes.objects.filter((s) => {
|
|
12109
13227
|
const a = appById.get(s.metadata?.applicationId);
|
|
12110
13228
|
return a?.stripeSubscriptionId === s.id && a?.stripeCustomerId === s.customer;
|
|
12111
13229
|
});
|
|
12112
|
-
const intents = new Map(
|
|
13230
|
+
const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
|
|
12113
13231
|
const amount = (s) => chapterBillingAmount(s, intents.get(s.metadata?.applicationId), env);
|
|
12114
13232
|
const subMs = (s) => (s.created ?? 0) * 1e3;
|
|
12115
13233
|
membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
|
|
@@ -12148,9 +13266,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
12148
13266
|
const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
|
|
12149
13267
|
if (!subsRes) return json({ error: "billing lookup failed upstream" }, 502);
|
|
12150
13268
|
const subById = new Map(subsRes.objects.map((s) => [s.id, s]));
|
|
12151
|
-
const intents = new Map(
|
|
13269
|
+
const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
|
|
12152
13270
|
const billingRows = [];
|
|
12153
|
-
for (const a of
|
|
13271
|
+
for (const a of rows5(data.applications)) {
|
|
12154
13272
|
if (!chapterBillingApplication(a, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
|
|
12155
13273
|
if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
|
|
12156
13274
|
const candidate = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
|
|
@@ -12186,9 +13304,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
12186
13304
|
return json({
|
|
12187
13305
|
billingReady: true,
|
|
12188
13306
|
testMode,
|
|
12189
|
-
truncated: !subsRes.complete ||
|
|
13307
|
+
truncated: !subsRes.complete || rows5(data.applications).length >= 200,
|
|
12190
13308
|
rows: billingRows,
|
|
12191
|
-
summary: subsRes.complete &&
|
|
13309
|
+
summary: subsRes.complete && rows5(data.applications).length < 200 ? summary : null
|
|
12192
13310
|
});
|
|
12193
13311
|
};
|
|
12194
13312
|
|
|
@@ -12580,7 +13698,7 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
|
12580
13698
|
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
12581
13699
|
function emailSpamSignals(copy) {
|
|
12582
13700
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12583
|
-
const
|
|
13701
|
+
const text11 = typeof copy.text === "string" ? copy.text : "";
|
|
12584
13702
|
const html = typeof copy.html === "string" ? copy.html : "";
|
|
12585
13703
|
const out = [];
|
|
12586
13704
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
@@ -12590,11 +13708,11 @@ function emailSpamSignals(copy) {
|
|
|
12590
13708
|
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
12591
13709
|
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
12592
13710
|
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
12593
|
-
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(
|
|
13711
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text11));
|
|
12594
13712
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12595
|
-
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(
|
|
12596
|
-
if (/!{2,}/.test(
|
|
12597
|
-
const urls = [...`${
|
|
13713
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text11))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
13714
|
+
if (/!{2,}/.test(text11)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
13715
|
+
const urls = [...`${text11}
|
|
12598
13716
|
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
12599
13717
|
const hosts = urls.map((url) => {
|
|
12600
13718
|
try {
|
|
@@ -12607,7 +13725,7 @@ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
|
12607
13725
|
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
12608
13726
|
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
12609
13727
|
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
12610
|
-
if (urls.length && words(
|
|
13728
|
+
if (urls.length && words(text11).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
12611
13729
|
return out;
|
|
12612
13730
|
}
|
|
12613
13731
|
function emailSenderWarnings(input) {
|
|
@@ -12647,8 +13765,8 @@ function validateEmailReadiness(input) {
|
|
|
12647
13765
|
${template.text ?? ""}
|
|
12648
13766
|
${html}`;
|
|
12649
13767
|
const sectionOnly = new Set(templateSectionNames2(copy));
|
|
12650
|
-
for (const
|
|
12651
|
-
problems.push({ code: "value-missing", variable:
|
|
13768
|
+
for (const name2 of templateVars2(copy)) if (name2 in input.groupValues && !input.groupValues[name2]?.trim() && !sectionOnly.has(name2)) {
|
|
13769
|
+
problems.push({ code: "value-missing", variable: name2, message: `Add the ${name2} used by this email.` });
|
|
12652
13770
|
}
|
|
12653
13771
|
}
|
|
12654
13772
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
@@ -12717,20 +13835,20 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12717
13835
|
if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
|
|
12718
13836
|
const templates = body.emailTemplates;
|
|
12719
13837
|
if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
|
|
12720
|
-
const
|
|
13838
|
+
const clean2 = {};
|
|
12721
13839
|
for (const [key3, value] of Object.entries(templates)) {
|
|
12722
13840
|
if (!events[key3]) continue;
|
|
12723
13841
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12724
13842
|
const t = value;
|
|
12725
13843
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12726
|
-
const subject = str2(t.subject).trim(),
|
|
13844
|
+
const subject = str2(t.subject).trim(), text11 = str2(t.text), html = str2(t.html);
|
|
12727
13845
|
if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
|
|
12728
|
-
const issues = validateEmailCopy4({ subject, text:
|
|
13846
|
+
const issues = validateEmailCopy4({ subject, text: text11 }, { allowedVariables: events[key3].variables });
|
|
12729
13847
|
const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
|
|
12730
13848
|
if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
|
|
12731
13849
|
return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
|
|
12732
13850
|
}
|
|
12733
|
-
|
|
13851
|
+
clean2[key3] = { subject, text: text11, ...html.trim() ? { html } : {}, enabled: t.enabled };
|
|
12734
13852
|
}
|
|
12735
13853
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12736
13854
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -12743,15 +13861,15 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12743
13861
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
12744
13862
|
if (ctx.chapter.mode === "hub") {
|
|
12745
13863
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
12746
|
-
await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...
|
|
13864
|
+
await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean2 }, notificationEmail, replyTo, debugEmail });
|
|
12747
13865
|
} else {
|
|
12748
13866
|
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
12749
13867
|
await db.transact([
|
|
12750
|
-
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates:
|
|
13868
|
+
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean2 } },
|
|
12751
13869
|
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
12752
13870
|
]);
|
|
12753
13871
|
}
|
|
12754
|
-
const warnings = Object.fromEntries(Object.entries(
|
|
13872
|
+
const warnings = Object.fromEntries(Object.entries(clean2).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
|
|
12755
13873
|
return json({ ok: true, warnings });
|
|
12756
13874
|
}
|
|
12757
13875
|
|
|
@@ -12789,8 +13907,8 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
|
12789
13907
|
const got = await gate5(req, env, ctx);
|
|
12790
13908
|
if (got instanceof Response) return got;
|
|
12791
13909
|
if (ctx.chapter.mode === "hub") {
|
|
12792
|
-
const
|
|
12793
|
-
return json({ sends:
|
|
13910
|
+
const rows6 = await listEmailLog({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
|
|
13911
|
+
return json({ sends: rows6.map((row2) => ({ ...row2, template: row2.templateId })) });
|
|
12794
13912
|
}
|
|
12795
13913
|
const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
|
|
12796
13914
|
const visible = [];
|
|
@@ -13020,7 +14138,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
13020
14138
|
return json({
|
|
13021
14139
|
targets: ctx.chapter.network.targets.map(({
|
|
13022
14140
|
id: id7,
|
|
13023
|
-
name,
|
|
14141
|
+
name: name2,
|
|
13024
14142
|
url: targetUrl,
|
|
13025
14143
|
fields,
|
|
13026
14144
|
sharedNotes,
|
|
@@ -13029,7 +14147,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
13029
14147
|
commercialParity
|
|
13030
14148
|
}) => ({
|
|
13031
14149
|
id: id7,
|
|
13032
|
-
name,
|
|
14150
|
+
name: name2,
|
|
13033
14151
|
url: targetUrl,
|
|
13034
14152
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
13035
14153
|
sharedNotes,
|
|
@@ -13368,8 +14486,8 @@ function terms(value) {
|
|
|
13368
14486
|
function matchesReaderIdentity(record4, search, includeEmail) {
|
|
13369
14487
|
const query = terms(search);
|
|
13370
14488
|
if (!query.length) return true;
|
|
13371
|
-
const
|
|
13372
|
-
const visible = [
|
|
14489
|
+
const name2 = folded(record4.name);
|
|
14490
|
+
const visible = [name2, ...name2.split(/\s+/u).filter(Boolean)];
|
|
13373
14491
|
if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
|
|
13374
14492
|
return query.every((term) => visible.some((candidate) => candidate.includes(term)));
|
|
13375
14493
|
}
|
|
@@ -13398,14 +14516,14 @@ async function listVisibleSearchPopulation(db, where, window, count) {
|
|
|
13398
14516
|
})));
|
|
13399
14517
|
return pages.flatMap((page) => (page[CRM_NS.record] ?? []).map(rowToRecord));
|
|
13400
14518
|
}
|
|
13401
|
-
async function listReaderRecords(db, type,
|
|
14519
|
+
async function listReaderRecords(db, type, allowed, emailField, window) {
|
|
13402
14520
|
const where = baseWhere(type, window.stage);
|
|
13403
14521
|
const crmDb = db;
|
|
13404
14522
|
const aggregate = await crmDb.aggregate(CRM_NS.record, { count: true }, { where });
|
|
13405
14523
|
const candidateCount = aggregate.count ?? 0;
|
|
13406
14524
|
if (window.search) {
|
|
13407
14525
|
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
13408
|
-
const includeEmail = Boolean(emailField &&
|
|
14526
|
+
const includeEmail = Boolean(emailField && allowed.includes(emailField));
|
|
13409
14527
|
const matches2 = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
|
|
13410
14528
|
return {
|
|
13411
14529
|
records: matches2.slice(window.offset, window.offset + window.limit),
|
|
@@ -13498,7 +14616,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13498
14616
|
const editable = reader.editableFields[input.type];
|
|
13499
14617
|
if (hasFields) {
|
|
13500
14618
|
const names = Object.keys(fields);
|
|
13501
|
-
if (!editable || names.length === 0 || names.length > 32 || names.some((
|
|
14619
|
+
if (!editable || names.length === 0 || names.length > 32 || names.some((name2) => !editable.includes(name2))) {
|
|
13502
14620
|
return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
|
|
13503
14621
|
}
|
|
13504
14622
|
} else if (!reader.stageTransitions.includes(input.type)) {
|
|
@@ -13563,7 +14681,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13563
14681
|
});
|
|
13564
14682
|
}
|
|
13565
14683
|
const capabilities = await operationalReaderRecord(record4, reader, db, env, ctx);
|
|
13566
|
-
if (hasFields && Object.keys(fields).some((
|
|
14684
|
+
if (hasFields && Object.keys(fields).some((name2) => !capabilities.edit?.fields.some((field) => field.id === name2))) {
|
|
13567
14685
|
return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
|
|
13568
14686
|
}
|
|
13569
14687
|
const deps = { crm: ctx.chapter.crm, db };
|
|
@@ -13618,8 +14736,8 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13618
14736
|
}
|
|
13619
14737
|
}
|
|
13620
14738
|
const type = url.searchParams.get("type") ?? "";
|
|
13621
|
-
const
|
|
13622
|
-
if (!IDENTIFIER.test(type) || !
|
|
14739
|
+
const allowed = reader.fields[type];
|
|
14740
|
+
if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
|
|
13623
14741
|
const recordId = url.searchParams.get("recordId");
|
|
13624
14742
|
if (recordId !== null) {
|
|
13625
14743
|
if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
|
|
@@ -13647,7 +14765,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13647
14765
|
page = await listReaderRecords(
|
|
13648
14766
|
db,
|
|
13649
14767
|
type,
|
|
13650
|
-
|
|
14768
|
+
allowed,
|
|
13651
14769
|
typeDef.emailField,
|
|
13652
14770
|
window
|
|
13653
14771
|
);
|
|
@@ -13728,8 +14846,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
13728
14846
|
async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
13729
14847
|
const fields = input.fields;
|
|
13730
14848
|
if (fields) {
|
|
13731
|
-
const
|
|
13732
|
-
if (!
|
|
14849
|
+
const allowed = target.editableFields[input.type];
|
|
14850
|
+
if (!allowed || Object.keys(fields).some((name2) => !allowed.includes(name2))) {
|
|
13733
14851
|
return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
|
|
13734
14852
|
}
|
|
13735
14853
|
} else if (!target.stageTransitions.includes(input.type)) {
|
|
@@ -14577,20 +15695,20 @@ async function networkPersonBulkSha256(value) {
|
|
|
14577
15695
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
14578
15696
|
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
14579
15697
|
}
|
|
14580
|
-
function assertNetworkPersonBulkRows(
|
|
14581
|
-
if (!Array.isArray(
|
|
15698
|
+
function assertNetworkPersonBulkRows(rows6) {
|
|
15699
|
+
if (!Array.isArray(rows6) || rows6.length < 1) {
|
|
14582
15700
|
throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
|
|
14583
15701
|
}
|
|
14584
|
-
if (
|
|
15702
|
+
if (rows6.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
|
|
14585
15703
|
throw new NetworkPersonRegistryError(
|
|
14586
15704
|
"bulk_rows_exceeded",
|
|
14587
15705
|
`bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
|
|
14588
15706
|
);
|
|
14589
15707
|
}
|
|
14590
|
-
return
|
|
15708
|
+
return rows6;
|
|
14591
15709
|
}
|
|
14592
|
-
async function networkPersonBulkBatchDigest(
|
|
14593
|
-
const bounded = assertNetworkPersonBulkRows(
|
|
15710
|
+
async function networkPersonBulkBatchDigest(rows6) {
|
|
15711
|
+
const bounded = assertNetworkPersonBulkRows(rows6);
|
|
14594
15712
|
return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
|
|
14595
15713
|
}
|
|
14596
15714
|
async function networkPersonRegistryStateDigest(state) {
|
|
@@ -14605,7 +15723,7 @@ async function networkPersonRegistryStateDigest(state) {
|
|
|
14605
15723
|
function rejectedValidation(errors) {
|
|
14606
15724
|
return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
|
|
14607
15725
|
}
|
|
14608
|
-
async function classifyNetworkPersonBulkRows(
|
|
15726
|
+
async function classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state) {
|
|
14609
15727
|
const aliasIds = new Set(state.aliases.map((alias) => alias.id));
|
|
14610
15728
|
const batchAliases = /* @__PURE__ */ new Set();
|
|
14611
15729
|
const claimOwners = /* @__PURE__ */ new Map();
|
|
@@ -14616,8 +15734,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
|
14616
15734
|
}
|
|
14617
15735
|
const results = [];
|
|
14618
15736
|
const values = [];
|
|
14619
|
-
for (let index2 = 0; index2 <
|
|
14620
|
-
const validation = validateNetworkPersonImport(
|
|
15737
|
+
for (let index2 = 0; index2 < rows6.length; index2 += 1) {
|
|
15738
|
+
const validation = validateNetworkPersonImport(rows6[index2], allowedChapterIds);
|
|
14621
15739
|
if (!validation.ok || !validation.value) {
|
|
14622
15740
|
const errors = validation.errors.slice(0, 8);
|
|
14623
15741
|
results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
|
|
@@ -14650,8 +15768,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
|
14650
15768
|
}
|
|
14651
15769
|
return { rows: results, values };
|
|
14652
15770
|
}
|
|
14653
|
-
async function previewNetworkPersonBulkImport(db,
|
|
14654
|
-
const bounded = assertNetworkPersonBulkRows(
|
|
15771
|
+
async function previewNetworkPersonBulkImport(db, rows6, allowedChapterIds) {
|
|
15772
|
+
const bounded = assertNetworkPersonBulkRows(rows6);
|
|
14655
15773
|
const state = await loadNetworkPersonRegistry(db);
|
|
14656
15774
|
const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
|
|
14657
15775
|
const counts = networkPersonBulkEmptyCounts();
|
|
@@ -14682,8 +15800,8 @@ function eventCounts(event2) {
|
|
|
14682
15800
|
}
|
|
14683
15801
|
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
14684
15802
|
if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
|
|
14685
|
-
const
|
|
14686
|
-
const batchDigest = await networkPersonBulkBatchDigest(
|
|
15803
|
+
const rows6 = assertNetworkPersonBulkRows(input.rows);
|
|
15804
|
+
const batchDigest = await networkPersonBulkBatchDigest(rows6);
|
|
14687
15805
|
if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
|
|
14688
15806
|
throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
|
|
14689
15807
|
}
|
|
@@ -14702,7 +15820,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14702
15820
|
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
14703
15821
|
batchDigest,
|
|
14704
15822
|
stateDigest: eventString(completed, "stateDigest"),
|
|
14705
|
-
rowCount: Number(completed.detail.rowCount) ||
|
|
15823
|
+
rowCount: Number(completed.detail.rowCount) || rows6.length,
|
|
14706
15824
|
counts: eventCounts(completed),
|
|
14707
15825
|
rows: [],
|
|
14708
15826
|
replayed: true
|
|
@@ -14710,7 +15828,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14710
15828
|
}
|
|
14711
15829
|
let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
14712
15830
|
if (!started) {
|
|
14713
|
-
const preview = await previewNetworkPersonBulkImport(db,
|
|
15831
|
+
const preview = await previewNetworkPersonBulkImport(db, rows6, allowedChapterIds);
|
|
14714
15832
|
if (preview.stateDigest !== input.expectedStateDigest) {
|
|
14715
15833
|
throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
|
|
14716
15834
|
}
|
|
@@ -14719,7 +15837,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14719
15837
|
networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
|
|
14720
15838
|
batchDigest,
|
|
14721
15839
|
stateDigest: input.expectedStateDigest,
|
|
14722
|
-
rowCount:
|
|
15840
|
+
rowCount: rows6.length,
|
|
14723
15841
|
previewCounts: preview.counts
|
|
14724
15842
|
})
|
|
14725
15843
|
], {
|
|
@@ -14735,7 +15853,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14735
15853
|
if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
|
|
14736
15854
|
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
|
|
14737
15855
|
}
|
|
14738
|
-
const classified = await classifyNetworkPersonBulkRows(
|
|
15856
|
+
const classified = await classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state);
|
|
14739
15857
|
const results = [];
|
|
14740
15858
|
const counts = networkPersonBulkEmptyCounts();
|
|
14741
15859
|
for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
|
|
@@ -14774,10 +15892,10 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14774
15892
|
}
|
|
14775
15893
|
if (counts.failed === 0) {
|
|
14776
15894
|
await db.transact([
|
|
14777
|
-
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now +
|
|
15895
|
+
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows6.length + 1, {
|
|
14778
15896
|
batchDigest,
|
|
14779
15897
|
stateDigest: input.expectedStateDigest,
|
|
14780
|
-
rowCount:
|
|
15898
|
+
rowCount: rows6.length,
|
|
14781
15899
|
counts
|
|
14782
15900
|
})
|
|
14783
15901
|
], { mutationId: completedMutationId });
|
|
@@ -14786,7 +15904,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14786
15904
|
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
14787
15905
|
batchDigest,
|
|
14788
15906
|
stateDigest: input.expectedStateDigest,
|
|
14789
|
-
rowCount:
|
|
15907
|
+
rowCount: rows6.length,
|
|
14790
15908
|
counts,
|
|
14791
15909
|
rows: results
|
|
14792
15910
|
};
|
|
@@ -14907,7 +16025,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
14907
16025
|
}
|
|
14908
16026
|
|
|
14909
16027
|
// src/worker-routes-network-person-registry.ts
|
|
14910
|
-
var
|
|
16028
|
+
var PATH7 = "/api/admin/network-person-registry";
|
|
14911
16029
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
14912
16030
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
14913
16031
|
var positiveRevision = (value) => {
|
|
@@ -14932,7 +16050,7 @@ function registryError(error) {
|
|
|
14932
16050
|
return null;
|
|
14933
16051
|
}
|
|
14934
16052
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
14935
|
-
if (url.pathname !==
|
|
16053
|
+
if (url.pathname !== PATH7) return null;
|
|
14936
16054
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
14937
16055
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
14938
16056
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -14944,9 +16062,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14944
16062
|
if (req.method === "POST") {
|
|
14945
16063
|
let body;
|
|
14946
16064
|
try {
|
|
14947
|
-
const
|
|
14948
|
-
if (new TextEncoder().encode(
|
|
14949
|
-
const parsed = JSON.parse(
|
|
16065
|
+
const text11 = await req.text();
|
|
16066
|
+
if (new TextEncoder().encode(text11).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
16067
|
+
const parsed = JSON.parse(text11);
|
|
14950
16068
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14951
16069
|
body = parsed;
|
|
14952
16070
|
} catch {
|
|
@@ -15115,7 +16233,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
|
|
|
15115
16233
|
// src/network-commercial-parity.ts
|
|
15116
16234
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
15117
16235
|
var MAX_RECORDS = 500;
|
|
15118
|
-
var
|
|
16236
|
+
var text9 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
15119
16237
|
function parityMessage(partition, kind, value) {
|
|
15120
16238
|
return [
|
|
15121
16239
|
"chapter-commercial-parity",
|
|
@@ -15147,18 +16265,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
15147
16265
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
15148
16266
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
15149
16267
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
15150
|
-
const eligible = input.rows.filter((row2) =>
|
|
16268
|
+
const eligible = input.rows.filter((row2) => text9(row2.id) && (text9(row2.stripeCustomerId) || text9(row2.stripeSubscriptionId)));
|
|
15151
16269
|
const selected2 = eligible.slice(0, maxRecords);
|
|
15152
16270
|
const key3 = await parityKey(input.secret);
|
|
15153
16271
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
15154
|
-
const applicationId =
|
|
15155
|
-
const customerId =
|
|
15156
|
-
const subscriptionId =
|
|
16272
|
+
const applicationId = text9(row2.id);
|
|
16273
|
+
const customerId = text9(row2.stripeCustomerId);
|
|
16274
|
+
const subscriptionId = text9(row2.stripeSubscriptionId);
|
|
15157
16275
|
return {
|
|
15158
16276
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
15159
16277
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
15160
16278
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
15161
|
-
status:
|
|
16279
|
+
status: text9(row2.status, 64),
|
|
15162
16280
|
canceled: row2.canceled === true
|
|
15163
16281
|
};
|
|
15164
16282
|
}));
|
|
@@ -15184,7 +16302,7 @@ function validSnapshot(snapshot, source) {
|
|
|
15184
16302
|
if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
|
|
15185
16303
|
const seen = /* @__PURE__ */ new Set();
|
|
15186
16304
|
for (const record4 of records) {
|
|
15187
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
16305
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text9(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
15188
16306
|
const key3 = recordKey(record4);
|
|
15189
16307
|
if (seen.has(key3)) return false;
|
|
15190
16308
|
seen.add(key3);
|
|
@@ -15195,16 +16313,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
15195
16313
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
15196
16314
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
15197
16315
|
const key3 = await parityKey(input.secret);
|
|
15198
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
15199
|
-
const customerId =
|
|
15200
|
-
const subscriptionId =
|
|
16316
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text9(row2.applicationId) && text9(row2.objectId)).map(async (row2) => {
|
|
16317
|
+
const customerId = text9(row2.customerId);
|
|
16318
|
+
const subscriptionId = text9(row2.subscriptionId) ?? text9(row2.objectId);
|
|
15201
16319
|
return {
|
|
15202
16320
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
15203
16321
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
15204
16322
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
15205
16323
|
status: null,
|
|
15206
16324
|
canceled: false,
|
|
15207
|
-
stripeStatus:
|
|
16325
|
+
stripeStatus: text9(row2.status, 64),
|
|
15208
16326
|
stripeCanceled: row2.status === "canceled"
|
|
15209
16327
|
};
|
|
15210
16328
|
}));
|
|
@@ -15286,14 +16404,14 @@ var PAGE2 = 500;
|
|
|
15286
16404
|
var MAX_SCAN = 5e3;
|
|
15287
16405
|
var MAX_RECORDS2 = 500;
|
|
15288
16406
|
async function localCommercialRows(db) {
|
|
15289
|
-
const
|
|
16407
|
+
const rows6 = [];
|
|
15290
16408
|
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
15291
16409
|
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
15292
16410
|
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
15293
|
-
|
|
15294
|
-
if (page.length < PAGE2) return { rows:
|
|
16411
|
+
rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
|
|
16412
|
+
if (page.length < PAGE2) return { rows: rows6, truncated: false };
|
|
15295
16413
|
}
|
|
15296
|
-
return { rows:
|
|
16414
|
+
return { rows: rows6, truncated: true };
|
|
15297
16415
|
}
|
|
15298
16416
|
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
15299
16417
|
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
@@ -15428,8 +16546,8 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
|
15428
16546
|
const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
|
|
15429
16547
|
return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
|
|
15430
16548
|
}
|
|
15431
|
-
const
|
|
15432
|
-
const member =
|
|
16549
|
+
const rows6 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
16550
|
+
const member = rows6.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15433
16551
|
if (!member) return { disposition: "not_applicable" };
|
|
15434
16552
|
const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
|
|
15435
16553
|
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
@@ -15673,15 +16791,15 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
15673
16791
|
if (submissionId && !SUBMISSION.test(submissionId)) {
|
|
15674
16792
|
return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
|
|
15675
16793
|
}
|
|
15676
|
-
const
|
|
15677
|
-
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !
|
|
16794
|
+
const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
|
|
16795
|
+
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
|
|
15678
16796
|
if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
|
|
15679
16797
|
for (const field of formation.required) {
|
|
15680
16798
|
const value = body[field];
|
|
15681
16799
|
if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
|
|
15682
16800
|
}
|
|
15683
16801
|
const input = {};
|
|
15684
|
-
for (const field of
|
|
16802
|
+
for (const field of allowed) {
|
|
15685
16803
|
const value = body[field];
|
|
15686
16804
|
if (value === void 0) continue;
|
|
15687
16805
|
const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
|
|
@@ -15745,10 +16863,10 @@ var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
|
|
|
15745
16863
|
var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
|
|
15746
16864
|
var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
|
|
15747
16865
|
var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15748
|
-
function unknownKeys3(value,
|
|
15749
|
-
for (const key3 of Object.keys(value)) if (!
|
|
16866
|
+
function unknownKeys3(value, allowed, path, errors) {
|
|
16867
|
+
for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15750
16868
|
}
|
|
15751
|
-
function
|
|
16869
|
+
function text10(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15752
16870
|
if (value === void 0 && !required) return void 0;
|
|
15753
16871
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15754
16872
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15774,7 +16892,7 @@ function scopeValue(value, path, errors) {
|
|
|
15774
16892
|
return { kind: raw2.kind };
|
|
15775
16893
|
}
|
|
15776
16894
|
if (raw2.kind === "chapter") {
|
|
15777
|
-
const chapterId =
|
|
16895
|
+
const chapterId = text10(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15778
16896
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15779
16897
|
return { kind: "chapter", chapterId };
|
|
15780
16898
|
}
|
|
@@ -15788,22 +16906,22 @@ function resourceValue(value, path, errors) {
|
|
|
15788
16906
|
return null;
|
|
15789
16907
|
}
|
|
15790
16908
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15791
|
-
const id7 =
|
|
16909
|
+
const id7 = text10(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15792
16910
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15793
|
-
const kind =
|
|
16911
|
+
const kind = text10(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15794
16912
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15795
|
-
const title =
|
|
15796
|
-
const summary =
|
|
15797
|
-
const status2 =
|
|
16913
|
+
const title = text10(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
16914
|
+
const summary = text10(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
16915
|
+
const status2 = text10(raw2.status, `${path}.status`, errors) ?? "";
|
|
15798
16916
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15799
16917
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15800
16918
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15801
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
16919
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text10(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15802
16920
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15803
16921
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15804
16922
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15805
|
-
const bodyMarkdown =
|
|
15806
|
-
const url =
|
|
16923
|
+
const bodyMarkdown = text10(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
16924
|
+
const url = text10(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15807
16925
|
if (url) {
|
|
15808
16926
|
try {
|
|
15809
16927
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15815,9 +16933,9 @@ function resourceValue(value, path, errors) {
|
|
|
15815
16933
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15816
16934
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15817
16935
|
const brandAsset = asset ? {
|
|
15818
|
-
bookId:
|
|
15819
|
-
assetId:
|
|
15820
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
16936
|
+
bookId: text10(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
16937
|
+
assetId: text10(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
16938
|
+
...asset.filename === void 0 ? {} : { filename: text10(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15821
16939
|
} : void 0;
|
|
15822
16940
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15823
16941
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15832,17 +16950,17 @@ function resourceValue(value, path, errors) {
|
|
|
15832
16950
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15833
16951
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15834
16952
|
const promotedFrom = promotedRaw ? {
|
|
15835
|
-
chapterId:
|
|
15836
|
-
resourceId:
|
|
16953
|
+
chapterId: text10(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
16954
|
+
resourceId: text10(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15837
16955
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15838
16956
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15839
|
-
promotedBy:
|
|
16957
|
+
promotedBy: text10(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15840
16958
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15841
16959
|
} : void 0;
|
|
15842
16960
|
const provenance2 = {
|
|
15843
|
-
createdBy:
|
|
16961
|
+
createdBy: text10(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15844
16962
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15845
|
-
updatedBy:
|
|
16963
|
+
updatedBy: text10(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15846
16964
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15847
16965
|
...promotedFrom ? { promotedFrom } : {}
|
|
15848
16966
|
};
|
|
@@ -15861,7 +16979,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15861
16979
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15862
16980
|
const sourceType = sourceRaw?.type;
|
|
15863
16981
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15864
|
-
const sourceAppId =
|
|
16982
|
+
const sourceAppId = text10(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15865
16983
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15866
16984
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15867
16985
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15869,8 +16987,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15869
16987
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15870
16988
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15871
16989
|
const target = targetRaw ? {
|
|
15872
|
-
appId:
|
|
15873
|
-
environment:
|
|
16990
|
+
appId: text10(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
16991
|
+
environment: text10(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15874
16992
|
} : void 0;
|
|
15875
16993
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15876
16994
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15958,17 +17076,17 @@ async function builderLibraryDigest(document) {
|
|
|
15958
17076
|
var control2 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
|
|
15959
17077
|
var local3 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
|
|
15960
17078
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
15961
|
-
const
|
|
15962
|
-
return Array.isArray(
|
|
17079
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
17080
|
+
return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
|
|
15963
17081
|
}
|
|
15964
17082
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
15965
|
-
const
|
|
15966
|
-
return control2(Array.isArray(
|
|
17083
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
17084
|
+
return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
15967
17085
|
}
|
|
15968
17086
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
15969
17087
|
const id7 = `builder-library-local:${environment}`;
|
|
15970
|
-
const
|
|
15971
|
-
return local3(Array.isArray(
|
|
17088
|
+
const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
|
|
17089
|
+
return local3(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
15972
17090
|
}
|
|
15973
17091
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
15974
17092
|
const { document, digest } = envelope;
|
|
@@ -17611,8 +18729,8 @@ var readable = /* @__PURE__ */ new Set([
|
|
|
17611
18729
|
"networkCommercialEvents",
|
|
17612
18730
|
"networkCommercialExceptions"
|
|
17613
18731
|
]);
|
|
17614
|
-
var check = (ns,
|
|
17615
|
-
if (!
|
|
18732
|
+
var check = (ns, allowed) => {
|
|
18733
|
+
if (!allowed.has(ns)) throw new TypeError("namespace outside membership authority contract");
|
|
17616
18734
|
return physical(ns);
|
|
17617
18735
|
};
|
|
17618
18736
|
function createMembershipAuthorityStorage(db, secretNames) {
|
|
@@ -17638,9 +18756,9 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
17638
18756
|
const guards = opts?.guards?.map((guard) => ({ ...guard, ns: check(guard.ns, readable) }));
|
|
17639
18757
|
return db.transact(mapped, { ...opts, ...guards ? { guards } : {} });
|
|
17640
18758
|
},
|
|
17641
|
-
secrets: { get(
|
|
17642
|
-
if (!secrets.has(
|
|
17643
|
-
return db.secrets.get(
|
|
18759
|
+
secrets: { get(name2) {
|
|
18760
|
+
if (!secrets.has(name2)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
|
|
18761
|
+
return db.secrets.get(name2);
|
|
17644
18762
|
} }
|
|
17645
18763
|
};
|
|
17646
18764
|
}
|
|
@@ -17810,7 +18928,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
|
|
|
17810
18928
|
const origin = new URL(options.origin), limit = options.limit ?? 100;
|
|
17811
18929
|
if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash || !Number.isSafeInteger(limit) || limit < 1 || limit > 2e3) throw new TypeError("invalid approval recovery configuration");
|
|
17812
18930
|
const db = ctx.makeDb(env);
|
|
17813
|
-
const
|
|
18931
|
+
const rows6 = (await db.query({ applications: { $: {
|
|
17814
18932
|
where: {
|
|
17815
18933
|
groupId: ctx.chapter.id,
|
|
17816
18934
|
stripeRuntime: env.ODLA_RUNTIME ?? "default",
|
|
@@ -17821,7 +18939,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
|
|
|
17821
18939
|
limit: limit + 1
|
|
17822
18940
|
} } })).applications ?? [];
|
|
17823
18941
|
let repaired = 0, pending = 0;
|
|
17824
|
-
for (const app of
|
|
18942
|
+
for (const app of rows6.slice(0, limit)) {
|
|
17825
18943
|
if (app.groupId !== ctx.chapter.id || app.stripeRuntime !== (env.ODLA_RUNTIME ?? "default") || app.status !== "approved" || app.approvalEffectsPending !== true) throw new Error("approval recovery scope mismatch");
|
|
17826
18944
|
try {
|
|
17827
18945
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { approvalEffectsLastAttemptAt: Date.now() } }], {
|
|
@@ -17846,7 +18964,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
|
|
|
17846
18964
|
pending++;
|
|
17847
18965
|
}
|
|
17848
18966
|
}
|
|
17849
|
-
return { repaired, pending, more:
|
|
18967
|
+
return { repaired, pending, more: rows6.length > limit };
|
|
17850
18968
|
}
|
|
17851
18969
|
|
|
17852
18970
|
// src/membership-chapter-delivery.ts
|
|
@@ -17862,7 +18980,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17862
18980
|
};
|
|
17863
18981
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17864
18982
|
let size = 0;
|
|
17865
|
-
let
|
|
18983
|
+
let text11 = "";
|
|
17866
18984
|
const decoder = new TextDecoder();
|
|
17867
18985
|
try {
|
|
17868
18986
|
while (true) {
|
|
@@ -17872,10 +18990,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17872
18990
|
if (done) break;
|
|
17873
18991
|
size += value.byteLength;
|
|
17874
18992
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17875
|
-
|
|
18993
|
+
text11 += decoder.decode(value, { stream: true });
|
|
17876
18994
|
}
|
|
17877
18995
|
try {
|
|
17878
|
-
return JSON.parse(
|
|
18996
|
+
return JSON.parse(text11 + decoder.decode());
|
|
17879
18997
|
} catch {
|
|
17880
18998
|
throw new TypeError(`invalid ${label}`);
|
|
17881
18999
|
}
|
|
@@ -17894,8 +19012,8 @@ import { validateEmailCopy as validateEmailCopy5 } from "@odla-ai/crm";
|
|
|
17894
19012
|
function invitationDelivery(env, recipientEmail) {
|
|
17895
19013
|
const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
|
|
17896
19014
|
if (env.ODLA_ENV !== "dev" || !env.SEND_INVITATION_TEST) return normal;
|
|
17897
|
-
const
|
|
17898
|
-
if (!
|
|
19015
|
+
const allowed = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
19016
|
+
if (!allowed.includes(recipientEmail.trim().toLowerCase())) return normal;
|
|
17899
19017
|
const to = env.ODLA_INVITATION_TEST_TO?.trim();
|
|
17900
19018
|
const from = env.ODLA_INVITATION_TEST_FROM?.trim();
|
|
17901
19019
|
const valid = (value) => value && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(value);
|
|
@@ -18415,27 +19533,27 @@ function validatedTokens(input) {
|
|
|
18415
19533
|
}
|
|
18416
19534
|
const tokens = {};
|
|
18417
19535
|
let serializedLength = 0;
|
|
18418
|
-
for (const [
|
|
18419
|
-
if (!TOKEN_NAME.test(
|
|
18420
|
-
throw new TypeError(`signup-control theme token "${
|
|
19536
|
+
for (const [name2, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
19537
|
+
if (!TOKEN_NAME.test(name2)) {
|
|
19538
|
+
throw new TypeError(`signup-control theme token "${name2}" must be an allowlisted --ui-* property`);
|
|
18421
19539
|
}
|
|
18422
19540
|
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
18423
|
-
throw new TypeError(`signup-control theme token "${
|
|
19541
|
+
throw new TypeError(`signup-control theme token "${name2}" must be a finite string or number`);
|
|
18424
19542
|
}
|
|
18425
19543
|
const value = String(raw2).trim();
|
|
18426
19544
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
18427
|
-
throw new TypeError(`signup-control theme token "${
|
|
19545
|
+
throw new TypeError(`signup-control theme token "${name2}" has an unsafe or oversized value`);
|
|
18428
19546
|
}
|
|
18429
|
-
serializedLength +=
|
|
19547
|
+
serializedLength += name2.length + value.length + 4;
|
|
18430
19548
|
if (serializedLength > MAX_SERIALIZED_LENGTH) {
|
|
18431
19549
|
throw new TypeError("signup-control theme exceeds the serialized size bound");
|
|
18432
19550
|
}
|
|
18433
|
-
tokens[
|
|
19551
|
+
tokens[name2] = value;
|
|
18434
19552
|
}
|
|
18435
19553
|
return tokens;
|
|
18436
19554
|
}
|
|
18437
19555
|
function cssFor(tokens) {
|
|
18438
|
-
const declarations = Object.entries(tokens).map(([
|
|
19556
|
+
const declarations = Object.entries(tokens).map(([name2, value]) => ` ${name2}: ${value};`);
|
|
18439
19557
|
return declarations.length ? `:root {
|
|
18440
19558
|
${declarations.join("\n")}
|
|
18441
19559
|
}` : "";
|
|
@@ -18550,6 +19668,7 @@ var BUILTIN_ROUTES = [
|
|
|
18550
19668
|
handleFormation,
|
|
18551
19669
|
handleMember,
|
|
18552
19670
|
handleMemberContent,
|
|
19671
|
+
handleMemberPortal,
|
|
18553
19672
|
handleGifts,
|
|
18554
19673
|
handleNamedSeat,
|
|
18555
19674
|
handleNamedSeatRegift,
|