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