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