@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.cjs
CHANGED
|
@@ -4,8 +4,8 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
6
|
var __export = (target, all) => {
|
|
7
|
-
for (var
|
|
8
|
-
__defProp(target,
|
|
7
|
+
for (var name2 in all)
|
|
8
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
var __copyProps = (to, from, except, desc) => {
|
|
11
11
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
@@ -70,6 +70,20 @@ __export(worker_exports, {
|
|
|
70
70
|
});
|
|
71
71
|
module.exports = __toCommonJS(worker_exports);
|
|
72
72
|
|
|
73
|
+
// src/admin-second-factor.ts
|
|
74
|
+
function sessionHasSecondFactor(payload) {
|
|
75
|
+
const fva = payload?.fva;
|
|
76
|
+
return Array.isArray(fva) && typeof fva[1] === "number" && fva[1] >= 0;
|
|
77
|
+
}
|
|
78
|
+
function adminSecondFactorAllows(mode, payload, who) {
|
|
79
|
+
if (mode === "off" || sessionHasSecondFactor(payload)) return true;
|
|
80
|
+
if (mode === "report") {
|
|
81
|
+
console.warn(JSON.stringify({ event: "chapter.admin_second_factor_missing", mode, who: who ?? null }));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
// src/worker-context.ts
|
|
74
88
|
var import_db = require("@odla-ai/db");
|
|
75
89
|
var import_jose = require("jose");
|
|
@@ -99,9 +113,9 @@ function canChangeRole(ctx) {
|
|
|
99
113
|
}
|
|
100
114
|
return { ok: true };
|
|
101
115
|
}
|
|
102
|
-
async function getVaultSecret(db,
|
|
116
|
+
async function getVaultSecret(db, name2) {
|
|
103
117
|
try {
|
|
104
|
-
const value = await db.secrets.get(
|
|
118
|
+
const value = await db.secrets.get(name2);
|
|
105
119
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
106
120
|
} catch {
|
|
107
121
|
return void 0;
|
|
@@ -434,7 +448,8 @@ function createWorkerContext(options) {
|
|
|
434
448
|
return await isAdminEmail(db, u.email) ? auth.adminRole : auth.ladder[0];
|
|
435
449
|
}
|
|
436
450
|
async function isAdmin(db, u) {
|
|
437
|
-
|
|
451
|
+
const admin = await isSuperAdminEmail(db, u.email, u.userId) || isAdminRole(await roleFor(db, u), auth);
|
|
452
|
+
return admin && adminSecondFactorAllows(auth.adminSecondFactor ?? "off", u.payload, u.email ?? u.userId);
|
|
438
453
|
}
|
|
439
454
|
function crmSender(env) {
|
|
440
455
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return void 0;
|
|
@@ -922,14 +937,14 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
922
937
|
const prior = latest.get(key3);
|
|
923
938
|
if (!prior || order(prior, fact) <= 0) latest.set(key3, row2);
|
|
924
939
|
}
|
|
925
|
-
const
|
|
926
|
-
const customers =
|
|
927
|
-
const subscriptions =
|
|
928
|
-
const invoices =
|
|
929
|
-
const seatPayments =
|
|
930
|
-
const disputes =
|
|
931
|
-
const refunds =
|
|
932
|
-
for (const charge of
|
|
940
|
+
const rows6 = [...latest.values()];
|
|
941
|
+
const customers = rows6.filter((row2) => row2.objectKind === "customer");
|
|
942
|
+
const subscriptions = rows6.filter((row2) => row2.objectKind === "subscription");
|
|
943
|
+
const invoices = rows6.filter((row2) => row2.objectKind === "invoice");
|
|
944
|
+
const seatPayments = rows6.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
|
|
945
|
+
const disputes = rows6.filter((row2) => row2.objectKind === "dispute");
|
|
946
|
+
const refunds = rows6.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
|
|
947
|
+
for (const charge of rows6.filter((row2) => row2.eventType === "charge.refunded")) {
|
|
933
948
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
934
949
|
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
935
950
|
}
|
|
@@ -1187,11 +1202,11 @@ async function readStripeRecoveryEvent(db, eventId) {
|
|
|
1187
1202
|
}
|
|
1188
1203
|
async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
|
|
1189
1204
|
const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
|
|
1190
|
-
const
|
|
1205
|
+
const rows6 = await db.query({
|
|
1191
1206
|
networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
|
|
1192
1207
|
networkCommercialExceptions: { $: { where: { id: exceptionId2 }, limit: 1 } }
|
|
1193
1208
|
});
|
|
1194
|
-
const stored =
|
|
1209
|
+
const stored = rows6.networkCommercialEvents?.[0], exception = rows6.networkCommercialExceptions?.[0];
|
|
1195
1210
|
if (!stored) throw new Error("stored event not found");
|
|
1196
1211
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
1197
1212
|
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
@@ -1267,7 +1282,7 @@ function findGiftRef(obj) {
|
|
|
1267
1282
|
const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
|
|
1268
1283
|
const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
|
|
1269
1284
|
const candidates = [metaOf(obj), metaOf(subscription)];
|
|
1270
|
-
const pickIdentity = (
|
|
1285
|
+
const pickIdentity = (name2) => candidates.map((metadata) => typeof metadata[name2] === "string" ? String(metadata[name2]) : void 0).find(Boolean);
|
|
1271
1286
|
const appId = pickIdentity("odlaAppId");
|
|
1272
1287
|
const environment = pickIdentity("odlaEnvironment");
|
|
1273
1288
|
const dataEnvironment = pickIdentity("odlaDataEnvironment");
|
|
@@ -1364,11 +1379,11 @@ async function archivedChapterMembershipEvent(db, rawBody, app, env, ctx) {
|
|
|
1364
1379
|
if (!app.membershipRestartId) return false;
|
|
1365
1380
|
const fact = normalizeNetworkCommercialStripeEvent(JSON.parse(rawBody), "");
|
|
1366
1381
|
if (!fact.subscriptionId || fact.subscriptionId === app.stripeSubscriptionId) return false;
|
|
1367
|
-
const
|
|
1382
|
+
const rows6 = (await db.query({ membershipCheckoutHistory: { $: { where: {
|
|
1368
1383
|
applicationId: app.id,
|
|
1369
1384
|
providerSubscriptionId: fact.subscriptionId
|
|
1370
1385
|
}, limit: 2 } } })).membershipCheckoutHistory ?? [];
|
|
1371
|
-
return
|
|
1386
|
+
return rows6.length === 1 && rows6[0].appId === env.ODLA_APP_ID && rows6[0].chapterId === ctx.chapter.id && rows6[0].environment === env.ODLA_ENV && rows6[0].runtime === stripeRuntimeIdentity(env.ODLA_RUNTIME);
|
|
1372
1387
|
}
|
|
1373
1388
|
|
|
1374
1389
|
// src/crm-sync.ts
|
|
@@ -1559,10 +1574,10 @@ async function findSharedRecord(deps, record4, tag) {
|
|
|
1559
1574
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1560
1575
|
}
|
|
1561
1576
|
const nameField = def.nameField ?? "name";
|
|
1562
|
-
const
|
|
1563
|
-
if (record4.type === "company" && typeof
|
|
1577
|
+
const name2 = record4.input[nameField];
|
|
1578
|
+
if (record4.type === "company" && typeof name2 === "string" && name2.trim()) {
|
|
1564
1579
|
const found = await deps.db.query({
|
|
1565
|
-
crm_record: { $: { where: { type: record4.type, name:
|
|
1580
|
+
crm_record: { $: { where: { type: record4.type, name: name2.trim() }, limit: 1 } }
|
|
1566
1581
|
});
|
|
1567
1582
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1568
1583
|
}
|
|
@@ -1700,27 +1715,81 @@ async function backfillCrm(deps) {
|
|
|
1700
1715
|
}
|
|
1701
1716
|
|
|
1702
1717
|
// src/email-retirement.ts
|
|
1703
|
-
function isRetiredEmailCopy(subject,
|
|
1718
|
+
function isRetiredEmailCopy(subject, text11) {
|
|
1704
1719
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
1705
|
-
const bodyCopy =
|
|
1720
|
+
const bodyCopy = text11.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
1706
1721
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
1707
1722
|
}
|
|
1708
1723
|
|
|
1709
1724
|
// src/email.ts
|
|
1725
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
1726
|
+
|
|
1727
|
+
// src/email-render.ts
|
|
1710
1728
|
var import_crm5 = require("@odla-ai/crm");
|
|
1711
1729
|
|
|
1730
|
+
// src/email-html.ts
|
|
1731
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1732
|
+
function escapeHtml(s) {
|
|
1733
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1734
|
+
}
|
|
1735
|
+
function lineHtml(line) {
|
|
1736
|
+
let out = "", last = 0;
|
|
1737
|
+
for (const match of line.matchAll(URL_RE)) {
|
|
1738
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
1739
|
+
const url = escapeHtml(match[0]);
|
|
1740
|
+
out += `<a href="${url}">${url}</a>`;
|
|
1741
|
+
last = match.index + match[0].length;
|
|
1742
|
+
}
|
|
1743
|
+
return out + escapeHtml(line.slice(last));
|
|
1744
|
+
}
|
|
1745
|
+
function textToHtml(text11) {
|
|
1746
|
+
const paragraphs = text11.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1747
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1748
|
+
return `<!doctype html>
|
|
1749
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1750
|
+
<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">
|
|
1751
|
+
${body}
|
|
1752
|
+
</body></html>`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/email-render.ts
|
|
1756
|
+
function render(template, vars) {
|
|
1757
|
+
return (0, import_crm5.renderTemplate)(template, vars);
|
|
1758
|
+
}
|
|
1759
|
+
var HTML_BODY_LIMIT = 2e5;
|
|
1760
|
+
function renderHtmlBody(template, vars) {
|
|
1761
|
+
const escaped = Object.fromEntries(Object.entries(vars).map(([name2, value]) => [name2, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
|
|
1762
|
+
return (0, import_crm5.renderTemplate)(template, escaped);
|
|
1763
|
+
}
|
|
1764
|
+
function htmlBodyIssues(html, options) {
|
|
1765
|
+
const issues = [];
|
|
1766
|
+
if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
|
|
1767
|
+
for (const message of (0, import_crm5.templateSectionIssues)(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
|
|
1768
|
+
const unknown = new Set((0, import_crm5.templateVars)(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
|
|
1769
|
+
for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
|
|
1770
|
+
if (options.values) {
|
|
1771
|
+
for (const name2 of (0, import_crm5.templateSectionNames)(html)) if (typeof options.values[name2] !== "string") issues.push({ code: "value-missing", variable: name2, message: `Missing event value: ${name2} (HTML body).` });
|
|
1772
|
+
for (const variable of (0, import_crm5.templateLiveVars)(html, options.values)) {
|
|
1773
|
+
if (unknown.has(variable)) continue;
|
|
1774
|
+
const value = options.values[variable];
|
|
1775
|
+
if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return issues;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1712
1781
|
// src/email-events.ts
|
|
1713
1782
|
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
1714
1783
|
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
1715
|
-
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
1716
|
-
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
1784
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
1785
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
1717
1786
|
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
1718
1787
|
var CHAPTER_EMAIL_EVENTS = {
|
|
1719
1788
|
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
1720
1789
|
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
1721
1790
|
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
1722
1791
|
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
1723
|
-
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
1792
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
|
|
1724
1793
|
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
1725
1794
|
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
1726
1795
|
};
|
|
@@ -1735,9 +1804,6 @@ function chapterEmailEvents(chapter) {
|
|
|
1735
1804
|
}
|
|
1736
1805
|
|
|
1737
1806
|
// src/email.ts
|
|
1738
|
-
function render(template, vars) {
|
|
1739
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
1740
|
-
}
|
|
1741
1807
|
function groupVars(group, vars) {
|
|
1742
1808
|
return {
|
|
1743
1809
|
...vars,
|
|
@@ -1763,14 +1829,16 @@ function planDelivery(input) {
|
|
|
1763
1829
|
const vars = groupVars(input.group, input.vars);
|
|
1764
1830
|
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
1765
1831
|
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
1766
|
-
const issues = (0,
|
|
1832
|
+
const issues = (0, import_crm6.validateEmailCopy)(tpl, {
|
|
1767
1833
|
allowedVariables: event2?.variables,
|
|
1768
1834
|
optionalVariables: event2?.optionalVariables,
|
|
1769
1835
|
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
1770
1836
|
values: vars,
|
|
1771
1837
|
tierId: vars.tierId
|
|
1772
1838
|
});
|
|
1773
|
-
|
|
1839
|
+
const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
|
|
1840
|
+
const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
|
|
1841
|
+
if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
|
|
1774
1842
|
const isProd = input.envName === "prod";
|
|
1775
1843
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
1776
1844
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
@@ -1778,37 +1846,14 @@ function planDelivery(input) {
|
|
|
1778
1846
|
const renderedSubject = render(tpl.subject, vars);
|
|
1779
1847
|
const renderedText = render(tpl.text, vars);
|
|
1780
1848
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
1781
|
-
if ((0,
|
|
1849
|
+
if ((0, import_crm6.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
1782
1850
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
1783
|
-
const
|
|
1851
|
+
const text11 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1784
1852
|
|
|
1785
1853
|
` + renderedText : renderedText;
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
// src/email-html.ts
|
|
1790
|
-
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1791
|
-
function escapeHtml(s) {
|
|
1792
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1793
|
-
}
|
|
1794
|
-
function lineHtml(line) {
|
|
1795
|
-
let out = "", last = 0;
|
|
1796
|
-
for (const match of line.matchAll(URL_RE)) {
|
|
1797
|
-
out += escapeHtml(line.slice(last, match.index));
|
|
1798
|
-
const url = escapeHtml(match[0]);
|
|
1799
|
-
out += `<a href="${url}">${url}</a>`;
|
|
1800
|
-
last = match.index + match[0].length;
|
|
1801
|
-
}
|
|
1802
|
-
return out + escapeHtml(line.slice(last));
|
|
1803
|
-
}
|
|
1804
|
-
function textToHtml(text10) {
|
|
1805
|
-
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1806
|
-
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1807
|
-
return `<!doctype html>
|
|
1808
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1809
|
-
<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">
|
|
1810
|
-
${body}
|
|
1811
|
-
</body></html>`;
|
|
1854
|
+
const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
|
|
1855
|
+
const htmlOut = renderedHtml && redirect ? `<p style="font:13px monospace;color:#555;margin:0 0 12px">(dev redirect; original recipient: ${escapeHtml(input.to)})</p>` + renderedHtml : renderedHtml;
|
|
1856
|
+
return { deliver: true, transport, to, subject, text: text11, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
|
|
1812
1857
|
}
|
|
1813
1858
|
|
|
1814
1859
|
// src/email-deliverability.ts
|
|
@@ -1828,10 +1873,10 @@ function deliverabilityHeaders(input) {
|
|
|
1828
1873
|
}
|
|
1829
1874
|
|
|
1830
1875
|
// src/notify.ts
|
|
1831
|
-
var
|
|
1876
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
1832
1877
|
|
|
1833
1878
|
// src/email-event-values.ts
|
|
1834
|
-
var
|
|
1879
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
1835
1880
|
|
|
1836
1881
|
// src/tiers.ts
|
|
1837
1882
|
function tierIsFree(tier) {
|
|
@@ -1844,9 +1889,9 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
1844
1889
|
function byOrder(a, b) {
|
|
1845
1890
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1846
1891
|
}
|
|
1847
|
-
function resolveTiers(group,
|
|
1848
|
-
const stored =
|
|
1849
|
-
if (
|
|
1892
|
+
function resolveTiers(group, rows6 = []) {
|
|
1893
|
+
const stored = rows6.filter((tier) => tier.active);
|
|
1894
|
+
if (rows6.length) return [...stored].sort(byOrder);
|
|
1850
1895
|
return [{
|
|
1851
1896
|
id: "standard",
|
|
1852
1897
|
groupId: String(group.id ?? ""),
|
|
@@ -1902,16 +1947,16 @@ function rowToTier(row2, groupId) {
|
|
|
1902
1947
|
}
|
|
1903
1948
|
async function loadTiers(db, group) {
|
|
1904
1949
|
const groupId = String(group.id ?? "");
|
|
1905
|
-
let
|
|
1950
|
+
let rows6;
|
|
1906
1951
|
try {
|
|
1907
|
-
|
|
1952
|
+
rows6 = (await db.query({
|
|
1908
1953
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
1909
1954
|
})).tiers;
|
|
1910
1955
|
} catch {
|
|
1911
1956
|
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
1912
1957
|
}
|
|
1913
|
-
if (!Array.isArray(
|
|
1914
|
-
const list =
|
|
1958
|
+
if (!Array.isArray(rows6)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
1959
|
+
const list = rows6.map((row2) => rowToTier(row2, groupId));
|
|
1915
1960
|
return resolveTiers(group, list);
|
|
1916
1961
|
}
|
|
1917
1962
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -1940,10 +1985,14 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1940
1985
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
1941
1986
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
1942
1987
|
}
|
|
1988
|
+
const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
|
|
1989
|
+
const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
|
|
1943
1990
|
return {
|
|
1944
1991
|
paymentAmount: formatted,
|
|
1945
1992
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
1946
|
-
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
1993
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
|
|
1994
|
+
namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
|
|
1995
|
+
namedSeatRecipientName
|
|
1947
1996
|
};
|
|
1948
1997
|
}
|
|
1949
1998
|
async function catalogTierName(db, group, app) {
|
|
@@ -1972,8 +2021,8 @@ function formatInstant(at2, timeZone) {
|
|
|
1972
2021
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
1973
2022
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
1974
2023
|
function applicantVariables(app) {
|
|
1975
|
-
const
|
|
1976
|
-
return { firstName:
|
|
2024
|
+
const text11 = (v) => typeof v === "string" ? v : "";
|
|
2025
|
+
return { firstName: text11(app.firstName), lastName: text11(app.lastName), email: text11(app.email), phone: text11(app.phone), state: text11(app.state) };
|
|
1977
2026
|
}
|
|
1978
2027
|
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
1979
2028
|
const startAt = formatInstant(now + 864e5, timeZone);
|
|
@@ -2001,6 +2050,8 @@ function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
|
2001
2050
|
purchaserName: "Sample Purchaser",
|
|
2002
2051
|
recipientName: "Sample Recipient",
|
|
2003
2052
|
chapterName: "Sample Chapter",
|
|
2053
|
+
namedSeat: "paid",
|
|
2054
|
+
namedSeatRecipientName: "Sample Recipient",
|
|
2004
2055
|
acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
|
|
2005
2056
|
joinUrl: `${origin}/join?seat=sample`
|
|
2006
2057
|
};
|
|
@@ -2017,8 +2068,8 @@ async function emailTierVariables(db, input) {
|
|
|
2017
2068
|
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
2018
2069
|
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
2019
2070
|
if (!tierId) return { tierName: "", tier: "" };
|
|
2020
|
-
const
|
|
2021
|
-
const tierName2 = (0,
|
|
2071
|
+
const name2 = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
2072
|
+
const tierName2 = (0, import_crm7.isEmailTierName)(name2, tierId) ? name2.trim() : "";
|
|
2022
2073
|
return { tierName: tierName2, tier: tierName2, tierId };
|
|
2023
2074
|
} catch {
|
|
2024
2075
|
return { tierName: "", tier: "" };
|
|
@@ -2026,8 +2077,8 @@ async function emailTierVariables(db, input) {
|
|
|
2026
2077
|
}
|
|
2027
2078
|
|
|
2028
2079
|
// src/notify.ts
|
|
2029
|
-
function fromAddress(configured,
|
|
2030
|
-
const display =
|
|
2080
|
+
function fromAddress(configured, name2) {
|
|
2081
|
+
const display = name2?.trim();
|
|
2031
2082
|
if (!display) return configured;
|
|
2032
2083
|
const angled = configured.match(/<([^>]+)>/);
|
|
2033
2084
|
return { name: display, email: (angled ? angled[1] : configured).trim() };
|
|
@@ -2078,7 +2129,7 @@ async function sendTemplated(deps, input) {
|
|
|
2078
2129
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
2079
2130
|
};
|
|
2080
2131
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
2081
|
-
if ((0,
|
|
2132
|
+
if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
2082
2133
|
try {
|
|
2083
2134
|
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
2084
2135
|
mutationId: `email-claim:${id7}:${attempt}`,
|
|
@@ -2098,14 +2149,14 @@ async function sendTemplated(deps, input) {
|
|
|
2098
2149
|
if (decision.transport === "log-only") return { sent: true };
|
|
2099
2150
|
let messageId;
|
|
2100
2151
|
try {
|
|
2101
|
-
const
|
|
2102
|
-
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(
|
|
2152
|
+
const text11 = String(row2.body);
|
|
2153
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text11);
|
|
2103
2154
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
2104
2155
|
const response = await deps.sender.send({
|
|
2105
2156
|
from: fromAddress(deps.from, input.group.fromName),
|
|
2106
2157
|
to: [decision.to],
|
|
2107
2158
|
subject: String(row2.subject),
|
|
2108
|
-
text:
|
|
2159
|
+
text: text11,
|
|
2109
2160
|
...html ? { html } : {},
|
|
2110
2161
|
...headers ? { headers } : {},
|
|
2111
2162
|
replyTo: input.group.replyTo
|
|
@@ -2385,13 +2436,13 @@ async function authoritativeState(db, input) {
|
|
|
2385
2436
|
return state;
|
|
2386
2437
|
}
|
|
2387
2438
|
async function currentPolicy(db, input, restartAware, state) {
|
|
2388
|
-
const
|
|
2439
|
+
const rows6 = await db.query({
|
|
2389
2440
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
2390
2441
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
2391
2442
|
});
|
|
2392
|
-
if (!Array.isArray(
|
|
2393
|
-
const offerings =
|
|
2394
|
-
const policies =
|
|
2443
|
+
if (!Array.isArray(rows6.membershipOfferingVersion) || !Array.isArray(rows6.discountPolicyVersion) || rows6.membershipOfferingVersion.length >= 200 || rows6.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
2444
|
+
const offerings = rows6.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
2445
|
+
const policies = rows6.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
|
|
2395
2446
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
2396
2447
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
2397
2448
|
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
@@ -2509,13 +2560,13 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
2509
2560
|
// src/checkout-named-seat.ts
|
|
2510
2561
|
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
2511
2562
|
const row2 = value;
|
|
2512
|
-
const
|
|
2563
|
+
const name2 = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
2513
2564
|
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
2514
2565
|
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
2515
|
-
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) ||
|
|
2566
|
+
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
|
|
2516
2567
|
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
2517
2568
|
}
|
|
2518
|
-
return { recipientName:
|
|
2569
|
+
return { recipientName: name2, recipientEmail: email };
|
|
2519
2570
|
}
|
|
2520
2571
|
async function checkoutNamedSeatIdentity(target, email) {
|
|
2521
2572
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -2956,14 +3007,14 @@ async function readMembershipAccess(db, input, now) {
|
|
|
2956
3007
|
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2957
3008
|
requireMembershipScope(entitlement, input);
|
|
2958
3009
|
const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
2959
|
-
const
|
|
3010
|
+
const allowed = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
|
|
2960
3011
|
return {
|
|
2961
3012
|
membershipId: id7,
|
|
2962
|
-
allowed
|
|
3013
|
+
allowed,
|
|
2963
3014
|
status: String(member.status),
|
|
2964
3015
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
2965
3016
|
revision: Number(member.revision),
|
|
2966
|
-
restartEligible: !
|
|
3017
|
+
restartEligible: !allowed && membershipCanRestart(member, now)
|
|
2967
3018
|
};
|
|
2968
3019
|
}
|
|
2969
3020
|
async function verifyMembershipAccessReceipt(value, input, now) {
|
|
@@ -3108,11 +3159,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
|
3108
3159
|
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
3109
3160
|
throw new Error("Stripe returned an invalid commercial list response");
|
|
3110
3161
|
}
|
|
3111
|
-
const
|
|
3112
|
-
if (
|
|
3113
|
-
objects.push(...
|
|
3162
|
+
const rows6 = page.data.map(asRaw).filter((row2) => row2 !== null);
|
|
3163
|
+
if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
3164
|
+
objects.push(...rows6);
|
|
3114
3165
|
if (!page.has_more) return { objects, complete: true };
|
|
3115
|
-
const cursor =
|
|
3166
|
+
const cursor = rows6.at(-1)?.id;
|
|
3116
3167
|
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
3117
3168
|
startingAfter = cursor;
|
|
3118
3169
|
}
|
|
@@ -3245,7 +3296,7 @@ async function approveApplication(input) {
|
|
|
3245
3296
|
try {
|
|
3246
3297
|
if (!duplicate) {
|
|
3247
3298
|
const write = await db.transact(
|
|
3248
|
-
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3299
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvedAt: Date.now(), approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3249
3300
|
{ mutationId: `approve:${id7}`, guards: [{
|
|
3250
3301
|
ns: "applications",
|
|
3251
3302
|
id: id7,
|
|
@@ -3528,8 +3579,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
3528
3579
|
throw new Error("gift issuance conflict");
|
|
3529
3580
|
}
|
|
3530
3581
|
}
|
|
3531
|
-
function buyerGiftClaims(
|
|
3532
|
-
return
|
|
3582
|
+
function buyerGiftClaims(rows6, now) {
|
|
3583
|
+
return rows6.map((row2) => {
|
|
3533
3584
|
const stored = String(row2.status ?? "pending");
|
|
3534
3585
|
const status2 = stored === "pending" && Number(row2.codeExpiresAt) <= now ? "expired" : stored;
|
|
3535
3586
|
return {
|
|
@@ -3675,8 +3726,8 @@ async function revokeGiftClaim(db, claim, reason, now) {
|
|
|
3675
3726
|
// src/payments-route-support.ts
|
|
3676
3727
|
async function findPaymentApplication(db, event2) {
|
|
3677
3728
|
if ("applicationId" in event2 && event2.applicationId) {
|
|
3678
|
-
const
|
|
3679
|
-
return
|
|
3729
|
+
const rows6 = await db.query({ applications: { $: { where: { id: event2.applicationId }, limit: 1 } } });
|
|
3730
|
+
return rows6.applications?.[0];
|
|
3680
3731
|
}
|
|
3681
3732
|
if (event2.kind !== "refunded" && "customerId" in event2 && event2.customerId) {
|
|
3682
3733
|
const { applications } = await db.query({ applications: { $: { where: { stripeCustomerId: event2.customerId }, limit: 2 } } });
|
|
@@ -3867,6 +3918,306 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
|
3867
3918
|
return membershipFail("membership_dispute_retry_required");
|
|
3868
3919
|
}
|
|
3869
3920
|
|
|
3921
|
+
// src/member-system-authority.ts
|
|
3922
|
+
var rows2 = (group) => group.fields.map((id7) => ({ ...group, id: id7 }));
|
|
3923
|
+
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
3924
|
+
...rows2({
|
|
3925
|
+
scope: "network",
|
|
3926
|
+
writer: "network_person",
|
|
3927
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3928
|
+
sensitivity: "pii",
|
|
3929
|
+
memberAccess: "edit",
|
|
3930
|
+
fields: [
|
|
3931
|
+
"person.legalName",
|
|
3932
|
+
"person.preferredName",
|
|
3933
|
+
"person.namePronunciation",
|
|
3934
|
+
"person.pronouns",
|
|
3935
|
+
"person.displayLegalName",
|
|
3936
|
+
"person.primaryEmail",
|
|
3937
|
+
"person.secondaryEmails",
|
|
3938
|
+
"person.phone",
|
|
3939
|
+
"person.linkedinUrl",
|
|
3940
|
+
"person.photo",
|
|
3941
|
+
"person.homeAddress",
|
|
3942
|
+
"person.fulfillmentFit",
|
|
3943
|
+
"person.fulfillmentSize"
|
|
3944
|
+
]
|
|
3945
|
+
}),
|
|
3946
|
+
...rows2({
|
|
3947
|
+
scope: "network",
|
|
3948
|
+
writer: "network_person",
|
|
3949
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3950
|
+
sensitivity: "ordinary",
|
|
3951
|
+
memberAccess: "edit",
|
|
3952
|
+
fields: [
|
|
3953
|
+
"person.title",
|
|
3954
|
+
"person.company",
|
|
3955
|
+
"person.companyObservedAt",
|
|
3956
|
+
"person.region",
|
|
3957
|
+
"person.whyText",
|
|
3958
|
+
"person.whyTokens",
|
|
3959
|
+
"person.skillsOffered",
|
|
3960
|
+
"person.openToRequests",
|
|
3961
|
+
"person.directoryPhotoConsent",
|
|
3962
|
+
"person.profilePrompts",
|
|
3963
|
+
"person.introducedByText"
|
|
3964
|
+
]
|
|
3965
|
+
}),
|
|
3966
|
+
...rows2({
|
|
3967
|
+
scope: "network",
|
|
3968
|
+
writer: "network_person",
|
|
3969
|
+
projections: ["bnf_admin", "chapter_admin"],
|
|
3970
|
+
sensitivity: "restricted",
|
|
3971
|
+
memberAccess: "none",
|
|
3972
|
+
fields: [
|
|
3973
|
+
"person.linkedinMemberId",
|
|
3974
|
+
"person.identityClaims",
|
|
3975
|
+
"person.aliases",
|
|
3976
|
+
"person.mergeHistory",
|
|
3977
|
+
"person.introducedByPersonId",
|
|
3978
|
+
"person.introducedAt",
|
|
3979
|
+
"person.firstTouch",
|
|
3980
|
+
"person.globalSuppression",
|
|
3981
|
+
"person.deceasedAt",
|
|
3982
|
+
"person.archivedAt"
|
|
3983
|
+
]
|
|
3984
|
+
}),
|
|
3985
|
+
...rows2({
|
|
3986
|
+
scope: "network",
|
|
3987
|
+
writer: "network_person",
|
|
3988
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3989
|
+
sensitivity: "sensitive",
|
|
3990
|
+
memberAccess: "edit",
|
|
3991
|
+
fields: ["person.accreditationSelfCertification", "person.accreditationCategory", "person.accreditationExpiresAt"]
|
|
3992
|
+
}),
|
|
3993
|
+
...rows2({
|
|
3994
|
+
scope: "network",
|
|
3995
|
+
writer: "network_person",
|
|
3996
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
3997
|
+
sensitivity: "restricted",
|
|
3998
|
+
memberAccess: "read",
|
|
3999
|
+
fields: ["person.acknowledgements"]
|
|
4000
|
+
}),
|
|
4001
|
+
...rows2({
|
|
4002
|
+
scope: "provider",
|
|
4003
|
+
writer: "clerk",
|
|
4004
|
+
projections: ["clerk_identity", "bnf_admin", "chapter_admin"],
|
|
4005
|
+
sensitivity: "restricted",
|
|
4006
|
+
memberAccess: "none",
|
|
4007
|
+
fields: ["identity.clerkUserIds", "identity.clerkEmailVerification"]
|
|
4008
|
+
}),
|
|
4009
|
+
...rows2({
|
|
4010
|
+
scope: "chapter",
|
|
4011
|
+
writer: "chapter_journey",
|
|
4012
|
+
projections: ["chapter_admin", "chapter_crm", "bnf_admin"],
|
|
4013
|
+
sensitivity: "ordinary",
|
|
4014
|
+
memberAccess: "none",
|
|
4015
|
+
fields: [
|
|
4016
|
+
"journey.chapterId",
|
|
4017
|
+
"journey.stage",
|
|
4018
|
+
"journey.stageEnteredAt",
|
|
4019
|
+
"journey.stageHistory",
|
|
4020
|
+
"journey.ending",
|
|
4021
|
+
"journey.endingReason",
|
|
4022
|
+
"journey.source",
|
|
4023
|
+
"journey.entryChapterId",
|
|
4024
|
+
"journey.route",
|
|
4025
|
+
"journey.flags",
|
|
4026
|
+
"journey.applicationStartedAt",
|
|
4027
|
+
"journey.appliedAt",
|
|
4028
|
+
"journey.paidAt",
|
|
4029
|
+
"journey.callBookedAt",
|
|
4030
|
+
"journey.metAt",
|
|
4031
|
+
"journey.approvedAt",
|
|
4032
|
+
"journey.onboardedAt",
|
|
4033
|
+
"journey.firstLoginAt",
|
|
4034
|
+
"journey.firstDealAt"
|
|
4035
|
+
]
|
|
4036
|
+
}),
|
|
4037
|
+
...rows2({
|
|
4038
|
+
scope: "chapter",
|
|
4039
|
+
writer: "chapter_journey",
|
|
4040
|
+
projections: ["chapter_admin", "chapter_crm"],
|
|
4041
|
+
sensitivity: "sensitive",
|
|
4042
|
+
memberAccess: "none",
|
|
4043
|
+
fields: [
|
|
4044
|
+
"journey.relationshipOwnerId",
|
|
4045
|
+
"journey.lastContactAt",
|
|
4046
|
+
"journey.nextAction",
|
|
4047
|
+
"journey.owes",
|
|
4048
|
+
"journey.asks",
|
|
4049
|
+
"journey.preparation",
|
|
4050
|
+
"journey.notes"
|
|
4051
|
+
]
|
|
4052
|
+
}),
|
|
4053
|
+
...rows2({
|
|
4054
|
+
scope: "chapter",
|
|
4055
|
+
writer: "chapter_journey",
|
|
4056
|
+
projections: ["chapter_admin", "chapter_crm", "member_self_service"],
|
|
4057
|
+
sensitivity: "ordinary",
|
|
4058
|
+
memberAccess: "edit",
|
|
4059
|
+
fields: ["journey.dealFlowConsent", "journey.directoryConsent", "journey.emailConsent", "journey.smsConsent"]
|
|
4060
|
+
}),
|
|
4061
|
+
...rows2({
|
|
4062
|
+
scope: "chapter",
|
|
4063
|
+
writer: "chapter_journey",
|
|
4064
|
+
projections: ["chapter_admin", "bnf_admin", "member_self_service"],
|
|
4065
|
+
sensitivity: "ordinary",
|
|
4066
|
+
memberAccess: "read",
|
|
4067
|
+
fields: ["journey.memberNumber"]
|
|
4068
|
+
}),
|
|
4069
|
+
...rows2({
|
|
4070
|
+
scope: "network",
|
|
4071
|
+
writer: "network_membership",
|
|
4072
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4073
|
+
sensitivity: "ordinary",
|
|
4074
|
+
memberAccess: "read",
|
|
4075
|
+
fields: [
|
|
4076
|
+
"membership.chapterId",
|
|
4077
|
+
"membership.journeyId",
|
|
4078
|
+
"membership.termStartsAt",
|
|
4079
|
+
"membership.termEndsAt",
|
|
4080
|
+
"membership.tier",
|
|
4081
|
+
"membership.status",
|
|
4082
|
+
"membership.joinedAt",
|
|
4083
|
+
"membership.autoRenews",
|
|
4084
|
+
"membership.renewalAt",
|
|
4085
|
+
"membership.cancelsAt",
|
|
4086
|
+
"membership.graceEndsAt",
|
|
4087
|
+
"membership.foundingCohort",
|
|
4088
|
+
"membership.offeringPolicyVersion",
|
|
4089
|
+
"membership.discountPolicyVersion"
|
|
4090
|
+
]
|
|
4091
|
+
}),
|
|
4092
|
+
...rows2({
|
|
4093
|
+
scope: "network",
|
|
4094
|
+
writer: "network_commercial_projection",
|
|
4095
|
+
projections: ["bnf_admin", "chapter_admin"],
|
|
4096
|
+
sensitivity: "restricted",
|
|
4097
|
+
memberAccess: "none",
|
|
4098
|
+
fields: [
|
|
4099
|
+
"membership.stripeCustomerId",
|
|
4100
|
+
"membership.stripeSubscriptionId",
|
|
4101
|
+
"membership.stripePriceId",
|
|
4102
|
+
"membership.lastInvoiceId",
|
|
4103
|
+
"membership.paymentOutcome",
|
|
4104
|
+
"membership.refundOutcome"
|
|
4105
|
+
]
|
|
4106
|
+
}),
|
|
4107
|
+
...rows2({
|
|
4108
|
+
scope: "network",
|
|
4109
|
+
writer: "network_discount_policy",
|
|
4110
|
+
projections: ["bnf_admin", "chapter_admin", "stripe_billing"],
|
|
4111
|
+
sensitivity: "ordinary",
|
|
4112
|
+
memberAccess: "none",
|
|
4113
|
+
fields: [
|
|
4114
|
+
"discountPolicy.chapterId",
|
|
4115
|
+
"discountPolicy.version",
|
|
4116
|
+
"discountPolicy.eligibleOfferingIds",
|
|
4117
|
+
"discountPolicy.percentOff",
|
|
4118
|
+
"discountPolicy.amountOff",
|
|
4119
|
+
"discountPolicy.activatesOn",
|
|
4120
|
+
"discountPolicy.invoiceCount",
|
|
4121
|
+
"discountPolicy.expiresAt",
|
|
4122
|
+
"discountPolicy.stackingOrder",
|
|
4123
|
+
"discountPolicy.status"
|
|
4124
|
+
]
|
|
4125
|
+
}),
|
|
4126
|
+
...rows2({
|
|
4127
|
+
scope: "network",
|
|
4128
|
+
writer: "network_entitlement",
|
|
4129
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service", "stripe_billing"],
|
|
4130
|
+
sensitivity: "ordinary",
|
|
4131
|
+
memberAccess: "read",
|
|
4132
|
+
fields: [
|
|
4133
|
+
"entitlement.membershipId",
|
|
4134
|
+
"entitlement.kind",
|
|
4135
|
+
"entitlement.priceCents",
|
|
4136
|
+
"entitlement.currency",
|
|
4137
|
+
"entitlement.payerPersonId",
|
|
4138
|
+
"entitlement.purchasedAt",
|
|
4139
|
+
"entitlement.termEndsAt",
|
|
4140
|
+
"entitlement.renewsWithMembership",
|
|
4141
|
+
"entitlement.status"
|
|
4142
|
+
]
|
|
4143
|
+
}),
|
|
4144
|
+
...rows2({
|
|
4145
|
+
scope: "network",
|
|
4146
|
+
writer: "network_seat",
|
|
4147
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4148
|
+
sensitivity: "pii",
|
|
4149
|
+
memberAccess: "read",
|
|
4150
|
+
fields: [
|
|
4151
|
+
"seat.membershipId",
|
|
4152
|
+
"seat.entitlementId",
|
|
4153
|
+
"seat.personId",
|
|
4154
|
+
"seat.recipientName",
|
|
4155
|
+
"seat.recipientEmail",
|
|
4156
|
+
"seat.role",
|
|
4157
|
+
"seat.status",
|
|
4158
|
+
"seat.acceptanceExpiresAt",
|
|
4159
|
+
"seat.acceptedAt",
|
|
4160
|
+
"seat.activatedAt",
|
|
4161
|
+
"seat.refundedAt"
|
|
4162
|
+
]
|
|
4163
|
+
}),
|
|
4164
|
+
...rows2({
|
|
4165
|
+
scope: "network",
|
|
4166
|
+
writer: "network_referral_ledger",
|
|
4167
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4168
|
+
sensitivity: "ordinary",
|
|
4169
|
+
memberAccess: "read",
|
|
4170
|
+
fields: [
|
|
4171
|
+
"referral.referrerPersonId",
|
|
4172
|
+
"referral.referredPersonId",
|
|
4173
|
+
"referral.qualifiedMembershipId",
|
|
4174
|
+
"referral.creditCents",
|
|
4175
|
+
"referral.status",
|
|
4176
|
+
"referral.appliedInvoiceId",
|
|
4177
|
+
"referral.carryForwardCents",
|
|
4178
|
+
"referral.adjustmentOf"
|
|
4179
|
+
]
|
|
4180
|
+
}),
|
|
4181
|
+
...rows2({
|
|
4182
|
+
scope: "network",
|
|
4183
|
+
writer: "network_investment_ledger",
|
|
4184
|
+
projections: ["bnf_admin", "chapter_admin", "member_self_service"],
|
|
4185
|
+
sensitivity: "restricted",
|
|
4186
|
+
memberAccess: "read",
|
|
4187
|
+
fields: [
|
|
4188
|
+
"investment.personId",
|
|
4189
|
+
"investment.chapterId",
|
|
4190
|
+
"investment.season",
|
|
4191
|
+
"investment.purpose",
|
|
4192
|
+
"investment.targetCount",
|
|
4193
|
+
"investment.status",
|
|
4194
|
+
"investment.amountCents",
|
|
4195
|
+
"investment.closedAt",
|
|
4196
|
+
"investment.sourceReference"
|
|
4197
|
+
]
|
|
4198
|
+
}),
|
|
4199
|
+
...rows2({
|
|
4200
|
+
scope: "provider",
|
|
4201
|
+
writer: "stripe",
|
|
4202
|
+
projections: ["stripe_billing", "bnf_admin", "chapter_admin"],
|
|
4203
|
+
sensitivity: "restricted",
|
|
4204
|
+
memberAccess: "none",
|
|
4205
|
+
fields: [
|
|
4206
|
+
"commercial.charge",
|
|
4207
|
+
"commercial.invoice",
|
|
4208
|
+
"commercial.refund",
|
|
4209
|
+
"commercial.providerSubscription",
|
|
4210
|
+
"commercial.paymentMethod"
|
|
4211
|
+
]
|
|
4212
|
+
})
|
|
4213
|
+
];
|
|
4214
|
+
var MEMBER_SELF_EDITABLE_FIELDS = new Set(
|
|
4215
|
+
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess === "edit").map((row2) => row2.id)
|
|
4216
|
+
);
|
|
4217
|
+
var MEMBER_SELF_READABLE_FIELDS = new Set(
|
|
4218
|
+
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
|
|
4219
|
+
);
|
|
4220
|
+
|
|
3870
4221
|
// src/member-system-contract.ts
|
|
3871
4222
|
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
3872
4223
|
const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
@@ -3878,8 +4229,8 @@ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink)
|
|
|
3878
4229
|
|
|
3879
4230
|
// src/stripe-webhook-partition.ts
|
|
3880
4231
|
var firstRow = async (db, ns, q) => {
|
|
3881
|
-
const
|
|
3882
|
-
return Array.isArray(
|
|
4232
|
+
const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4233
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3883
4234
|
};
|
|
3884
4235
|
var stripeEventIdentity = (event2) => ({
|
|
3885
4236
|
...event2.appId ? { appId: event2.appId } : {},
|
|
@@ -3986,8 +4337,8 @@ async function replayedStripeReceipt(db, eventId, digest, event2, legacyDigest,
|
|
|
3986
4337
|
// src/stripe-event-reconcile.ts
|
|
3987
4338
|
async function firstRow2(db, ns, q) {
|
|
3988
4339
|
const res = await db.query({ [ns]: { $: q } });
|
|
3989
|
-
const
|
|
3990
|
-
return Array.isArray(
|
|
4340
|
+
const rows6 = res[ns];
|
|
4341
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3991
4342
|
}
|
|
3992
4343
|
async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
3993
4344
|
const db = ctx.makeDb(env);
|
|
@@ -4304,19 +4655,19 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
4304
4655
|
const db = raw2, target = stripeEventTarget(env, ctx);
|
|
4305
4656
|
const local4 = (row3) => row3.chapterId === target.chapterId && row3.appId === target.appId && row3.environment === target.environment && row3.runtime === target.runtime;
|
|
4306
4657
|
if (req.method === "GET") {
|
|
4307
|
-
const
|
|
4658
|
+
const rows6 = (await db.query({ stripeEventReceipts: { $: {
|
|
4308
4659
|
where: { chapterId: target.chapterId, runtime: target.runtime },
|
|
4309
4660
|
order: { receivedAt: "desc" },
|
|
4310
4661
|
limit: 500
|
|
4311
4662
|
} } })).stripeEventReceipts ?? [];
|
|
4312
|
-
const pending =
|
|
4663
|
+
const pending = rows6.filter((row3) => local4(row3) && (String(row3.disposition).startsWith("quarantine_") || row3.retryReady === true));
|
|
4313
4664
|
return json({ rows: pending.map((row3) => ({
|
|
4314
4665
|
eventId: row3.eventId,
|
|
4315
4666
|
kind: row3.kind,
|
|
4316
4667
|
disposition: row3.disposition,
|
|
4317
4668
|
receivedAt: row3.receivedAt,
|
|
4318
4669
|
retryReady: row3.retryReady === true
|
|
4319
|
-
})), truncated:
|
|
4670
|
+
})), truncated: rows6.length >= 500 });
|
|
4320
4671
|
}
|
|
4321
4672
|
let eventId;
|
|
4322
4673
|
try {
|
|
@@ -4502,9 +4853,9 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
4502
4853
|
var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
|
|
4503
4854
|
async function memberSessionApplication(db, chapterId, runtime, identity2) {
|
|
4504
4855
|
const lookup = async (where) => {
|
|
4505
|
-
const
|
|
4506
|
-
if (!Array.isArray(
|
|
4507
|
-
return
|
|
4856
|
+
const rows6 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
|
|
4857
|
+
if (!Array.isArray(rows6) || rows6.length >= 100) throw new Error("member identity lookup incomplete");
|
|
4858
|
+
return rows6.filter((row2) => local(row2, chapterId, runtime));
|
|
4508
4859
|
};
|
|
4509
4860
|
let apps = await lookup({ clerkUserId: identity2.userId });
|
|
4510
4861
|
let emailFallback = false;
|
|
@@ -4881,10 +5232,10 @@ async function namedSeatRecipientClaimId(input, email) {
|
|
|
4881
5232
|
}
|
|
4882
5233
|
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4883
5234
|
membershipClock(now);
|
|
4884
|
-
const
|
|
5235
|
+
const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4885
5236
|
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4886
5237
|
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4887
|
-
if (
|
|
5238
|
+
if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
4888
5239
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4889
5240
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4890
5241
|
requireMembershipScope(member, input);
|
|
@@ -4901,7 +5252,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4901
5252
|
id: id7,
|
|
4902
5253
|
...membershipScope(input),
|
|
4903
5254
|
primaryMembershipId: memberId,
|
|
4904
|
-
recipientName:
|
|
5255
|
+
recipientName: name2,
|
|
4905
5256
|
recipientEmailNormalized: email,
|
|
4906
5257
|
purchaserUserId: input.purchaserUserId,
|
|
4907
5258
|
recipientClaimId: claimId,
|
|
@@ -4948,7 +5299,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4948
5299
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4949
5300
|
}
|
|
4950
5301
|
requireMembershipScope(seat, input);
|
|
4951
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !==
|
|
5302
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name2 || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4952
5303
|
return seat;
|
|
4953
5304
|
}
|
|
4954
5305
|
|
|
@@ -5139,9 +5490,9 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
5139
5490
|
membershipClock(now);
|
|
5140
5491
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
5141
5492
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
5142
|
-
const
|
|
5493
|
+
const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5143
5494
|
let lifted = 0;
|
|
5144
|
-
for (const member of
|
|
5495
|
+
for (const member of rows6.slice(0, limit)) {
|
|
5145
5496
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
5146
5497
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
5147
5498
|
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
@@ -5157,7 +5508,7 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
5157
5508
|
] });
|
|
5158
5509
|
lifted++;
|
|
5159
5510
|
}
|
|
5160
|
-
return { lifted, more:
|
|
5511
|
+
return { lifted, more: rows6.length > limit };
|
|
5161
5512
|
}
|
|
5162
5513
|
|
|
5163
5514
|
// src/membership-seat-reprice.ts
|
|
@@ -5170,9 +5521,9 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
5170
5521
|
const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
|
|
5171
5522
|
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
5172
5523
|
const target = current.seatPriceCents;
|
|
5173
|
-
const
|
|
5174
|
-
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more:
|
|
5175
|
-
for (const member of
|
|
5524
|
+
const rows6 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5525
|
+
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows6.length > limit };
|
|
5526
|
+
for (const member of rows6.slice(0, limit)) {
|
|
5176
5527
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
5177
5528
|
const id7 = String(member.id), skip = (reason) => {
|
|
5178
5529
|
receipt.skipped.push({ membershipId: id7, reason });
|
|
@@ -5271,8 +5622,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
5271
5622
|
}
|
|
5272
5623
|
}
|
|
5273
5624
|
if (!one) {
|
|
5274
|
-
const
|
|
5275
|
-
return json({ seats:
|
|
5625
|
+
const rows6 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
5626
|
+
return json({ seats: rows6.slice(0, 200).map(project), truncated: rows6.length > 200 });
|
|
5276
5627
|
}
|
|
5277
5628
|
const seat = await membershipLoad(db, "networkSeat", one[1]);
|
|
5278
5629
|
if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
|
|
@@ -5491,8 +5842,8 @@ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
|
|
|
5491
5842
|
const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
|
|
5492
5843
|
const found = await db.query({ networkMembership: page, networkSeat: page });
|
|
5493
5844
|
const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
|
|
5494
|
-
const
|
|
5495
|
-
const disputes =
|
|
5845
|
+
const rows6 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
|
|
5846
|
+
const disputes = rows6.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
|
|
5496
5847
|
return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
|
|
5497
5848
|
}
|
|
5498
5849
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
@@ -5542,9 +5893,9 @@ function stateOf(item, all, now, window, rivals) {
|
|
|
5542
5893
|
var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
|
|
5543
5894
|
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
5544
5895
|
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
5545
|
-
function normalize(
|
|
5896
|
+
function normalize(rows6, collection, parse, invalid2) {
|
|
5546
5897
|
const out = [];
|
|
5547
|
-
for (const row2 of
|
|
5898
|
+
for (const row2 of rows6) {
|
|
5548
5899
|
try {
|
|
5549
5900
|
out.push({ value: parse(row2.snapshot), row: row2 });
|
|
5550
5901
|
} catch (error) {
|
|
@@ -5555,11 +5906,11 @@ function normalize(rows5, collection, parse, invalid2) {
|
|
|
5555
5906
|
}
|
|
5556
5907
|
async function listMembershipPolicyVersions(db, input) {
|
|
5557
5908
|
const where = input.chapterId ? { chapterId: input.chapterId } : {};
|
|
5558
|
-
const
|
|
5909
|
+
const rows6 = await db.query({
|
|
5559
5910
|
membershipOfferingVersion: { $: { where, limit: LIMIT } },
|
|
5560
5911
|
discountPolicyVersion: { $: { where, limit: LIMIT } }
|
|
5561
5912
|
});
|
|
5562
|
-
const offeringRows =
|
|
5913
|
+
const offeringRows = rows6.membershipOfferingVersion ?? [], policyRows = rows6.discountPolicyVersion ?? [];
|
|
5563
5914
|
const invalid2 = [];
|
|
5564
5915
|
const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
|
|
5565
5916
|
const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
|
|
@@ -5699,22 +6050,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
5699
6050
|
const skip = (membershipId, reason) => {
|
|
5700
6051
|
receipt.skipped.push({ membershipId, reason });
|
|
5701
6052
|
};
|
|
5702
|
-
let
|
|
6053
|
+
let rows6;
|
|
5703
6054
|
if (input.membershipIds) {
|
|
5704
|
-
|
|
6055
|
+
rows6 = [];
|
|
5705
6056
|
for (const id7 of input.membershipIds) {
|
|
5706
6057
|
const member = await membershipLoad(db, "networkMembership", id7);
|
|
5707
6058
|
if (!member) skip(id7, "not_found");
|
|
5708
6059
|
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
5709
|
-
else
|
|
6060
|
+
else rows6.push(member);
|
|
5710
6061
|
}
|
|
5711
6062
|
} else {
|
|
5712
6063
|
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5713
6064
|
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
5714
6065
|
receipt.more = found.length > limit;
|
|
5715
|
-
|
|
6066
|
+
rows6 = found.slice(0, limit);
|
|
5716
6067
|
}
|
|
5717
|
-
for (const member of
|
|
6068
|
+
for (const member of rows6) {
|
|
5718
6069
|
const id7 = String(member.id);
|
|
5719
6070
|
if (member.discountPolicyVersionId === policy2.id) {
|
|
5720
6071
|
receipt.unchanged.push(id7);
|
|
@@ -6003,20 +6354,20 @@ function belongsToSignup(account, applicationId) {
|
|
|
6003
6354
|
}
|
|
6004
6355
|
|
|
6005
6356
|
// src/member.ts
|
|
6006
|
-
var
|
|
6357
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
6007
6358
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
6008
6359
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
6009
6360
|
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
6010
6361
|
function resolveApplication(a) {
|
|
6011
6362
|
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
6012
6363
|
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
6013
|
-
for (const [
|
|
6364
|
+
for (const [name2, arr] of [["required", required], ["optional", optional]]) {
|
|
6014
6365
|
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
6015
|
-
throw new Error(`defineChapter.application.${
|
|
6366
|
+
throw new Error(`defineChapter.application.${name2}: must be an array of field-name strings`);
|
|
6016
6367
|
}
|
|
6017
6368
|
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
6018
6369
|
if (unsafe) {
|
|
6019
|
-
throw new Error(`defineChapter.application.${
|
|
6370
|
+
throw new Error(`defineChapter.application.${name2}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
6020
6371
|
}
|
|
6021
6372
|
}
|
|
6022
6373
|
const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
@@ -6037,7 +6388,7 @@ function resolveApplication(a) {
|
|
|
6037
6388
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
6038
6389
|
throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
|
|
6039
6390
|
}
|
|
6040
|
-
(0,
|
|
6391
|
+
(0, import_crm9.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
|
|
6041
6392
|
}
|
|
6042
6393
|
}
|
|
6043
6394
|
return {
|
|
@@ -6068,19 +6419,19 @@ function hasDisclaimerAck(fields) {
|
|
|
6068
6419
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
6069
6420
|
function applicantProfile(chapter, fields) {
|
|
6070
6421
|
const app = chapter.application;
|
|
6071
|
-
const
|
|
6422
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
6072
6423
|
const profile = {};
|
|
6073
6424
|
for (const f of [...app.required, ...app.optional]) {
|
|
6074
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
6425
|
+
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
6075
6426
|
const v = fields[f];
|
|
6076
6427
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
6077
6428
|
}
|
|
6078
|
-
if (fields.focus !== void 0 &&
|
|
6429
|
+
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
6079
6430
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
6080
6431
|
}
|
|
6081
6432
|
async function submitApplication(db, chapter, fields, opts) {
|
|
6082
6433
|
const app = chapter.application;
|
|
6083
|
-
const fieldStates = (0,
|
|
6434
|
+
const fieldStates = (0, import_crm9.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
6084
6435
|
for (const [f, state] of Object.entries(fieldStates)) {
|
|
6085
6436
|
if (!state.required) continue;
|
|
6086
6437
|
const v = fields[f];
|
|
@@ -6194,10 +6545,10 @@ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
|
|
|
6194
6545
|
applications: { $: { where: { emailNormalized: email }, limit: 10 } },
|
|
6195
6546
|
$users: { $: { where: { email }, limit: 2 } }
|
|
6196
6547
|
});
|
|
6197
|
-
const
|
|
6548
|
+
const rows6 = data.applications ?? [], others = rows6.filter((row2) => row2.id !== applicationId);
|
|
6198
6549
|
const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
|
|
6199
6550
|
if (others.length !== closed.length) return conflict();
|
|
6200
|
-
const original =
|
|
6551
|
+
const original = rows6.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
|
|
6201
6552
|
const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
|
|
6202
6553
|
if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
|
|
6203
6554
|
const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
|
|
@@ -6221,7 +6572,7 @@ async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
|
6221
6572
|
}
|
|
6222
6573
|
|
|
6223
6574
|
// src/worker-routes.ts
|
|
6224
|
-
var
|
|
6575
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
6225
6576
|
|
|
6226
6577
|
// src/discussion-reference-security.ts
|
|
6227
6578
|
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
@@ -6321,15 +6672,15 @@ async function signupControlDigest(document) {
|
|
|
6321
6672
|
|
|
6322
6673
|
// src/admission-store-read.ts
|
|
6323
6674
|
async function readAdmissionRows(db, namespace, where, maximum) {
|
|
6324
|
-
const
|
|
6675
|
+
const rows6 = [];
|
|
6325
6676
|
for (let offset = 0; offset <= maximum; offset += 500) {
|
|
6326
6677
|
const result = await db.query({ [namespace]: { $: { where, order: { id: "asc" }, limit: 500, offset } } });
|
|
6327
6678
|
const page = Array.isArray(result[namespace]) ? result[namespace] : [];
|
|
6328
|
-
|
|
6329
|
-
if (
|
|
6679
|
+
rows6.push(...page);
|
|
6680
|
+
if (rows6.length > maximum) throw new Error("admission_assignment_limit");
|
|
6330
6681
|
if (page.length < 500) break;
|
|
6331
6682
|
}
|
|
6332
|
-
return
|
|
6683
|
+
return rows6;
|
|
6333
6684
|
}
|
|
6334
6685
|
|
|
6335
6686
|
// src/admission-grants.ts
|
|
@@ -6412,7 +6763,7 @@ async function createAdmissionGrantEnvelope(document) {
|
|
|
6412
6763
|
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
6413
6764
|
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
6414
6765
|
}
|
|
6415
|
-
var
|
|
6766
|
+
var rows3 = (value) => Array.isArray(value) ? value : [];
|
|
6416
6767
|
function asGrant(row2) {
|
|
6417
6768
|
if (typeof row2.id !== "string" || typeof row2.emailNormalized !== "string") return null;
|
|
6418
6769
|
if (row2.sourceScope !== "local" && row2.sourceScope !== "parent") return null;
|
|
@@ -6423,7 +6774,7 @@ async function findAdmissionGrant(db, targetId, email, now) {
|
|
|
6423
6774
|
const emailNormalized = normalizeAdmissionEmail(email);
|
|
6424
6775
|
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
6425
6776
|
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
6426
|
-
const active =
|
|
6777
|
+
const active = rows3(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
6427
6778
|
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
6428
6779
|
)).sort((a, b) => {
|
|
6429
6780
|
const scope2 = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
@@ -6437,7 +6788,7 @@ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
|
6437
6788
|
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
6438
6789
|
}
|
|
6439
6790
|
const found = await db.query({ admissionGrantRevisions: {} });
|
|
6440
|
-
return
|
|
6791
|
+
return rows3(found.admissionGrantRevisions).some((row2) => row2.sourceId === sourceId && row2.targetId === targetId && row2.environment === environment);
|
|
6441
6792
|
}
|
|
6442
6793
|
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
6443
6794
|
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
@@ -6446,7 +6797,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
|
|
|
6446
6797
|
order: { revision: "desc" },
|
|
6447
6798
|
limit: 1
|
|
6448
6799
|
} } });
|
|
6449
|
-
const row2 =
|
|
6800
|
+
const row2 = rows3(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
6450
6801
|
return row2 ? row2 : null;
|
|
6451
6802
|
}
|
|
6452
6803
|
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
@@ -6525,8 +6876,8 @@ var APPLICATION_LABELS = {
|
|
|
6525
6876
|
state: "State"
|
|
6526
6877
|
};
|
|
6527
6878
|
function humanize(id7) {
|
|
6528
|
-
const
|
|
6529
|
-
return
|
|
6879
|
+
const text11 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
6880
|
+
return text11 ? `${text11[0].toUpperCase()}${text11.slice(1)}` : id7;
|
|
6530
6881
|
}
|
|
6531
6882
|
function applicationDefs(chapter) {
|
|
6532
6883
|
try {
|
|
@@ -6619,8 +6970,8 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
6619
6970
|
errors.push(`${path} must be an object`);
|
|
6620
6971
|
continue;
|
|
6621
6972
|
}
|
|
6622
|
-
const
|
|
6623
|
-
for (const key3 of Object.keys(item)) if (!
|
|
6973
|
+
const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
6974
|
+
for (const key3 of Object.keys(item)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6624
6975
|
const id7 = text5(item.id, `${path}.id`, errors, 80);
|
|
6625
6976
|
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6626
6977
|
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
@@ -6684,27 +7035,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
6684
7035
|
if (db.aggregate) {
|
|
6685
7036
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
6686
7037
|
}
|
|
6687
|
-
const
|
|
6688
|
-
return Array.isArray(
|
|
7038
|
+
const rows6 = (await db.query({ signupControls: {} })).signupControls;
|
|
7039
|
+
return Array.isArray(rows6) && rows6.some((row2) => row2.targetId === targetId && row2.environment === environment && (!active || row2.active === true));
|
|
6689
7040
|
}
|
|
6690
7041
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
6691
7042
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
6692
|
-
const
|
|
7043
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
6693
7044
|
where: { targetId, environment },
|
|
6694
7045
|
order: { revision: "desc" },
|
|
6695
7046
|
limit: 1
|
|
6696
7047
|
} } })).signupControls;
|
|
6697
|
-
const latest = Array.isArray(
|
|
7048
|
+
const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
6698
7049
|
return asStored(latest);
|
|
6699
7050
|
}
|
|
6700
7051
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
6701
7052
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
6702
|
-
const
|
|
7053
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
6703
7054
|
where: { targetId, environment, active: true },
|
|
6704
7055
|
order: { revision: "desc" },
|
|
6705
7056
|
limit: 1
|
|
6706
7057
|
} } })).signupControls;
|
|
6707
|
-
const active = Array.isArray(
|
|
7058
|
+
const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
6708
7059
|
return asStored(active);
|
|
6709
7060
|
}
|
|
6710
7061
|
function controlledApplication(base, document) {
|
|
@@ -7077,17 +7428,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
7077
7428
|
if (!reader) return "";
|
|
7078
7429
|
const decoder = new TextDecoder();
|
|
7079
7430
|
let bytes = 0;
|
|
7080
|
-
let
|
|
7431
|
+
let text11 = "";
|
|
7081
7432
|
for (; ; ) {
|
|
7082
7433
|
const { done, value } = await reader.read();
|
|
7083
7434
|
if (req.signal.aborted) return invalid2();
|
|
7084
7435
|
if (done) {
|
|
7085
7436
|
complete = true;
|
|
7086
|
-
return
|
|
7437
|
+
return text11 + decoder.decode();
|
|
7087
7438
|
}
|
|
7088
7439
|
bytes += value.byteLength;
|
|
7089
7440
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
7090
|
-
|
|
7441
|
+
text11 += decoder.decode(value, { stream: true });
|
|
7091
7442
|
}
|
|
7092
7443
|
} catch {
|
|
7093
7444
|
return invalid2();
|
|
@@ -7191,9 +7542,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
|
7191
7542
|
let selection;
|
|
7192
7543
|
if (request?.body) {
|
|
7193
7544
|
try {
|
|
7194
|
-
const
|
|
7195
|
-
if (
|
|
7196
|
-
const body = JSON.parse(
|
|
7545
|
+
const text11 = await readBoundedRequestText(request, 2048);
|
|
7546
|
+
if (text11 instanceof Response) return text11;
|
|
7547
|
+
const body = JSON.parse(text11);
|
|
7197
7548
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
7198
7549
|
selection = body.namedSeat;
|
|
7199
7550
|
} catch {
|
|
@@ -7346,9 +7697,9 @@ async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
|
|
|
7346
7697
|
if (!app || app.groupId !== chapterId || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(runtime)) {
|
|
7347
7698
|
throw new Error("invitation purchaser unavailable");
|
|
7348
7699
|
}
|
|
7349
|
-
const
|
|
7350
|
-
if (!
|
|
7351
|
-
return
|
|
7700
|
+
const name2 = [app.firstName, app.lastName].filter((value) => typeof value === "string").join(" ").replace(/\s+/g, " ").trim();
|
|
7701
|
+
if (!name2) throw new Error("invitation purchaser name unavailable");
|
|
7702
|
+
return name2.slice(0, 160);
|
|
7352
7703
|
}
|
|
7353
7704
|
async function namedSeatApplicationContext(req, seatId, env, ctx) {
|
|
7354
7705
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
@@ -7377,10 +7728,10 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
|
|
|
7377
7728
|
}
|
|
7378
7729
|
async function existingNamedSeatApplication(context, env, ctx) {
|
|
7379
7730
|
const { db, userId, email, invitation } = context;
|
|
7380
|
-
const
|
|
7381
|
-
const apps =
|
|
7731
|
+
const rows6 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
|
|
7732
|
+
const apps = rows6.filter((row2) => !closedApplication(row2));
|
|
7382
7733
|
if (!apps.length) {
|
|
7383
|
-
const closed =
|
|
7734
|
+
const closed = rows6.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
7384
7735
|
return { predecessor: closed ? { id: String(closed.id) } : null };
|
|
7385
7736
|
}
|
|
7386
7737
|
const app = apps[0];
|
|
@@ -7745,6 +8096,111 @@ async function notifySubmitConfirmed(db, env, notice) {
|
|
|
7745
8096
|
}
|
|
7746
8097
|
}
|
|
7747
8098
|
|
|
8099
|
+
// src/member-entitlement.ts
|
|
8100
|
+
function entitledTierId(app, now = Date.now()) {
|
|
8101
|
+
const granted = typeof app.entitlementTierId === "string" && app.entitlementTierId ? app.entitlementTierId : null;
|
|
8102
|
+
const endsAt = typeof app.entitlementTierEndsAt === "number" ? app.entitlementTierEndsAt : null;
|
|
8103
|
+
if (granted && (endsAt === null || endsAt > now)) return granted;
|
|
8104
|
+
return typeof app.tierId === "string" && app.tierId ? app.tierId : null;
|
|
8105
|
+
}
|
|
8106
|
+
|
|
8107
|
+
// src/portal-member.ts
|
|
8108
|
+
var text6 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8109
|
+
async function activeHubGrant(db, identity2) {
|
|
8110
|
+
const byUser = (await db.query({ hubMemberGrants: { $: { where: { userId: identity2.userId }, limit: 5 } } })).hubMemberGrants ?? [];
|
|
8111
|
+
const byEmail = identity2.email ? (await db.query({ hubMemberGrants: { $: { where: { emailNormalized: normalizeChapterEmail(identity2.email) }, limit: 1 } } })).hubMemberGrants ?? [] : [];
|
|
8112
|
+
return [...byUser, ...byEmail].find((row2) => row2.status === "active") ?? null;
|
|
8113
|
+
}
|
|
8114
|
+
async function resolvePortalMember(db, env, ctx, identity2) {
|
|
8115
|
+
try {
|
|
8116
|
+
if (ctx.chapter.mode === "hub") {
|
|
8117
|
+
const grant = await activeHubGrant(db, identity2);
|
|
8118
|
+
if (!grant || !identity2.email) return { status: "not-member" };
|
|
8119
|
+
return { status: "member", member: {
|
|
8120
|
+
memberId: String(grant.id),
|
|
8121
|
+
source: "hub-grant",
|
|
8122
|
+
email: identity2.email,
|
|
8123
|
+
...text6(grant.firstName) ? { firstName: text6(grant.firstName) } : {},
|
|
8124
|
+
...text6(grant.lastName) ? { lastName: text6(grant.lastName) } : {},
|
|
8125
|
+
tierId: null,
|
|
8126
|
+
memberSince: Number(grant.grantedAt) || null,
|
|
8127
|
+
row: grant
|
|
8128
|
+
} };
|
|
8129
|
+
}
|
|
8130
|
+
const session = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, identity2);
|
|
8131
|
+
if (!session) return { status: "not-member" };
|
|
8132
|
+
const app = session.source;
|
|
8133
|
+
if (!await chapterMembershipAccess(db, env, ctx, app)) return { status: "not-member", row: app };
|
|
8134
|
+
return { status: "member", member: {
|
|
8135
|
+
memberId: String(app.id),
|
|
8136
|
+
source: "application",
|
|
8137
|
+
email: String(app.email ?? identity2.email ?? ""),
|
|
8138
|
+
...text6(app.firstName) ? { firstName: text6(app.firstName) } : {},
|
|
8139
|
+
...text6(app.lastName) ? { lastName: text6(app.lastName) } : {},
|
|
8140
|
+
tierId: entitledTierId(app),
|
|
8141
|
+
memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
|
|
8142
|
+
row: app
|
|
8143
|
+
} };
|
|
8144
|
+
} catch {
|
|
8145
|
+
return { status: "unavailable" };
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
function initialsOf(name2) {
|
|
8149
|
+
const words2 = name2.trim().split(/\s+/).filter(Boolean);
|
|
8150
|
+
const letters = words2.length > 1 ? [words2[0], words2[words2.length - 1]] : words2;
|
|
8151
|
+
return letters.map((word) => [...word][0].toUpperCase()).join("") || "?";
|
|
8152
|
+
}
|
|
8153
|
+
async function portalIdentity(db, ctx, member) {
|
|
8154
|
+
const profile = (await db.query({ memberProfiles: { $: { where: { id: member.memberId }, limit: 1 } } })).memberProfiles?.[0];
|
|
8155
|
+
let tier = null;
|
|
8156
|
+
if (member.tierId) {
|
|
8157
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
8158
|
+
const match = group ? (await loadTiers(db, group)).find((candidate) => candidate.id === member.tierId) : void 0;
|
|
8159
|
+
tier = { id: member.tierId, name: match?.name ?? member.tierId };
|
|
8160
|
+
}
|
|
8161
|
+
const legal = [member.firstName, member.lastName].filter(Boolean).join(" ");
|
|
8162
|
+
const displayName = text6(profile?.preferredName) ?? (legal || member.email);
|
|
8163
|
+
const photoPath = text6(profile?.photoPath);
|
|
8164
|
+
const photoUrl = photoPath && db.storage && ctx.chapter.memberPortal?.photos ? await db.storage.sign(photoPath, 3600).catch(() => null) : null;
|
|
8165
|
+
return {
|
|
8166
|
+
memberId: member.memberId,
|
|
8167
|
+
tier,
|
|
8168
|
+
memberSince: member.memberSince,
|
|
8169
|
+
displayName,
|
|
8170
|
+
firstName: text6(profile?.preferredName)?.split(/\s+/)[0] ?? member.firstName ?? null,
|
|
8171
|
+
initials: initialsOf(displayName),
|
|
8172
|
+
location: text6(profile?.location) ?? null,
|
|
8173
|
+
photoUrl
|
|
8174
|
+
};
|
|
8175
|
+
}
|
|
8176
|
+
|
|
8177
|
+
// src/portal-me.ts
|
|
8178
|
+
var servesMembers = (ctx) => ctx.chapter.mode === "chapter" || Boolean(ctx.chapter.memberPortal || ctx.chapter.memberContent);
|
|
8179
|
+
async function hubMe(db, env, ctx, identity2, base) {
|
|
8180
|
+
if (!servesMembers(ctx)) return base;
|
|
8181
|
+
const result = await resolvePortalMember(db, env, ctx, identity2);
|
|
8182
|
+
if (result.status === "unavailable") return { ...base, memberAccess: null, portal: null };
|
|
8183
|
+
if (result.status !== "member") return { ...base, memberAccess: false, portal: null };
|
|
8184
|
+
return { ...base, memberAccess: true, portal: await identityOrNull(db, ctx, result.member) };
|
|
8185
|
+
}
|
|
8186
|
+
async function chapterPortalIdentity(db, ctx, memberAccess, app) {
|
|
8187
|
+
if (!memberAccess || !app || !ctx.chapter.memberPortal) return null;
|
|
8188
|
+
const text11 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8189
|
+
return identityOrNull(db, ctx, {
|
|
8190
|
+
memberId: String(app.id),
|
|
8191
|
+
source: "application",
|
|
8192
|
+
email: String(app.email ?? ""),
|
|
8193
|
+
...text11(app.firstName) ? { firstName: text11(app.firstName) } : {},
|
|
8194
|
+
...text11(app.lastName) ? { lastName: text11(app.lastName) } : {},
|
|
8195
|
+
tierId: entitledTierId(app),
|
|
8196
|
+
memberSince: Number(app.approvedAt ?? app.namedSeatAcceptedAt ?? app.createdAt) || null,
|
|
8197
|
+
row: app
|
|
8198
|
+
});
|
|
8199
|
+
}
|
|
8200
|
+
async function identityOrNull(db, ctx, member) {
|
|
8201
|
+
return portalIdentity(db, ctx, member).catch(() => null);
|
|
8202
|
+
}
|
|
8203
|
+
|
|
7748
8204
|
// src/worker-routes-health.ts
|
|
7749
8205
|
var handleConfig = async (_req, url, env, ctx) => {
|
|
7750
8206
|
if (url.pathname !== "/api/config") return null;
|
|
@@ -7759,13 +8215,13 @@ var handleHealth = async (_req, url, env, ctx) => {
|
|
|
7759
8215
|
if (url.pathname !== "/api/health") return null;
|
|
7760
8216
|
const response = json({ ok: true });
|
|
7761
8217
|
response.headers.set("cache-control", "no-store");
|
|
7762
|
-
for (const [
|
|
8218
|
+
for (const [name2, value] of Object.entries({
|
|
7763
8219
|
"x-odla-app-id": env.ODLA_APP_ID,
|
|
7764
8220
|
"x-odla-chapter-id": ctx.chapter.id,
|
|
7765
8221
|
"x-odla-data-environment": env.ODLA_ENV,
|
|
7766
8222
|
"x-odla-runtime": env.ODLA_RUNTIME || "default"
|
|
7767
8223
|
})) {
|
|
7768
|
-
if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(
|
|
8224
|
+
if (typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value)) response.headers.set(name2, value);
|
|
7769
8225
|
}
|
|
7770
8226
|
return response;
|
|
7771
8227
|
};
|
|
@@ -7779,8 +8235,10 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
7779
8235
|
const db = ctx.makeDb(env);
|
|
7780
8236
|
const role = await ctx.roleFor(db, u);
|
|
7781
8237
|
const superAdmin = await ctx.isSuperAdminEmail(db, u.email, u.userId);
|
|
7782
|
-
const
|
|
7783
|
-
|
|
8238
|
+
const admin = superAdmin || isAdminRole(role, ctx.auth);
|
|
8239
|
+
const secondFactorOk = !admin || adminSecondFactorAllows(ctx.auth.adminSecondFactor ?? "off", u.payload, u.email);
|
|
8240
|
+
const base = { authorized: admin && secondFactorOk, role, superAdmin, email: u.email ?? null, ...admin && !secondFactorOk ? { secondFactorRequired: true } : {} };
|
|
8241
|
+
if (ctx.chapter.mode !== "chapter") return json(await hubMe(db, env, ctx, u, base));
|
|
7784
8242
|
let member;
|
|
7785
8243
|
try {
|
|
7786
8244
|
member = await memberSessionApplication(
|
|
@@ -7816,13 +8274,14 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
7816
8274
|
} catch {
|
|
7817
8275
|
}
|
|
7818
8276
|
}
|
|
7819
|
-
|
|
8277
|
+
const portal = await chapterPortalIdentity(db, ctx, memberAccess, member?.source);
|
|
8278
|
+
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, portal, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
|
|
7820
8279
|
};
|
|
7821
8280
|
var handleCrm = async (req, url, env, ctx) => {
|
|
7822
8281
|
const crmBase = ctx.crmBase;
|
|
7823
8282
|
if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + "/")) return null;
|
|
7824
8283
|
const db = ctx.makeDb(env);
|
|
7825
|
-
const routes = (0,
|
|
8284
|
+
const routes = (0, import_crm10.createCrmRoutes)({
|
|
7826
8285
|
crm: ctx.chapter.crm,
|
|
7827
8286
|
db,
|
|
7828
8287
|
authorize: async (r) => {
|
|
@@ -8061,9 +8520,9 @@ var MAX_BENEFITS = 24;
|
|
|
8061
8520
|
var MAX_MARKDOWN = 65536;
|
|
8062
8521
|
var MAX_LABEL = 160;
|
|
8063
8522
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8064
|
-
function rejectUnknownKeys(value,
|
|
8523
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
8065
8524
|
for (const key3 of Object.keys(value)) {
|
|
8066
|
-
if (!
|
|
8525
|
+
if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8067
8526
|
}
|
|
8068
8527
|
}
|
|
8069
8528
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -8075,10 +8534,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
8075
8534
|
errors.push(`${path} must be a string`);
|
|
8076
8535
|
return void 0;
|
|
8077
8536
|
}
|
|
8078
|
-
const
|
|
8079
|
-
if (required && !
|
|
8080
|
-
if (
|
|
8081
|
-
return
|
|
8537
|
+
const text11 = value.trim();
|
|
8538
|
+
if (required && !text11) errors.push(`${path} is required`);
|
|
8539
|
+
if (text11.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
8540
|
+
return text11;
|
|
8082
8541
|
}
|
|
8083
8542
|
function validateMembershipPageDocument(input, options = {}) {
|
|
8084
8543
|
const errors = [];
|
|
@@ -8127,8 +8586,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
8127
8586
|
else {
|
|
8128
8587
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
8129
8588
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
8130
|
-
const
|
|
8131
|
-
return
|
|
8589
|
+
const text11 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
8590
|
+
return text11 ? [text11] : [];
|
|
8132
8591
|
});
|
|
8133
8592
|
}
|
|
8134
8593
|
}
|
|
@@ -8219,10 +8678,10 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
8219
8678
|
]);
|
|
8220
8679
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
|
|
8221
8680
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8222
|
-
function unknownKeys(value,
|
|
8223
|
-
for (const key3 of Object.keys(value)) if (!
|
|
8681
|
+
function unknownKeys(value, allowed, path, errors) {
|
|
8682
|
+
for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8224
8683
|
}
|
|
8225
|
-
function
|
|
8684
|
+
function text7(value, path, errors, max = 65536) {
|
|
8226
8685
|
if (typeof value !== "string" || !value.trim()) {
|
|
8227
8686
|
errors.push(`${path} is required`);
|
|
8228
8687
|
return "";
|
|
@@ -8245,11 +8704,11 @@ function tiersOf(value, errors) {
|
|
|
8245
8704
|
return [];
|
|
8246
8705
|
}
|
|
8247
8706
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
8248
|
-
const id7 =
|
|
8707
|
+
const id7 = text7(item.id, `${path}.id`, errors, 80);
|
|
8249
8708
|
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8250
8709
|
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
8251
8710
|
seen.add(id7);
|
|
8252
|
-
const
|
|
8711
|
+
const name2 = text7(item.name, `${path}.name`, errors, 160);
|
|
8253
8712
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
8254
8713
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
8255
8714
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -8267,10 +8726,10 @@ function tiersOf(value, errors) {
|
|
|
8267
8726
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
8268
8727
|
errors.push(`${path}.interval must be year or month`);
|
|
8269
8728
|
}
|
|
8270
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
8729
|
+
const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
8271
8730
|
return [{
|
|
8272
8731
|
id: id7,
|
|
8273
|
-
name,
|
|
8732
|
+
name: name2,
|
|
8274
8733
|
available: item.available === true,
|
|
8275
8734
|
sortOrder: Number(item.sortOrder) || 0,
|
|
8276
8735
|
priceCents: Number(item.priceCents) || 0,
|
|
@@ -8313,7 +8772,7 @@ function policyOf(value, errors, tiers) {
|
|
|
8313
8772
|
errors.push("document.policy must be an object");
|
|
8314
8773
|
return void 0;
|
|
8315
8774
|
}
|
|
8316
|
-
const
|
|
8775
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
8317
8776
|
"disclaimerText",
|
|
8318
8777
|
"refundPolicyText",
|
|
8319
8778
|
"merchantDisclosureText",
|
|
@@ -8321,11 +8780,11 @@ function policyOf(value, errors, tiers) {
|
|
|
8321
8780
|
"commitmentText",
|
|
8322
8781
|
"normsText"
|
|
8323
8782
|
]);
|
|
8324
|
-
unknownKeys(input, /* @__PURE__ */ new Set([...
|
|
8783
|
+
unknownKeys(input, /* @__PURE__ */ new Set([...allowed, "tierPolicies"]), "document.policy", errors);
|
|
8325
8784
|
const result = {};
|
|
8326
|
-
for (const key3 of
|
|
8785
|
+
for (const key3 of allowed) {
|
|
8327
8786
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
8328
|
-
result[key3] =
|
|
8787
|
+
result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
|
|
8329
8788
|
}
|
|
8330
8789
|
const tierPolicies = {};
|
|
8331
8790
|
if (input.tierPolicies !== void 0) {
|
|
@@ -8340,8 +8799,8 @@ function policyOf(value, errors, tiers) {
|
|
|
8340
8799
|
}
|
|
8341
8800
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
8342
8801
|
tierPolicies[id7] = {
|
|
8343
|
-
refundPolicyText:
|
|
8344
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
8802
|
+
refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
8803
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
8345
8804
|
};
|
|
8346
8805
|
}
|
|
8347
8806
|
}
|
|
@@ -8381,9 +8840,9 @@ function validateSignupControlDocument(input) {
|
|
|
8381
8840
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
8382
8841
|
const source = recordOf(root.source);
|
|
8383
8842
|
const target = recordOf(root.target);
|
|
8384
|
-
const appId =
|
|
8385
|
-
const chapterId =
|
|
8386
|
-
const environment =
|
|
8843
|
+
const appId = text7(source?.appId, "document.source.appId", errors, 63);
|
|
8844
|
+
const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
8845
|
+
const environment = text7(target?.environment, "document.target.environment", errors, 63);
|
|
8387
8846
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
8388
8847
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
8389
8848
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -8524,14 +8983,45 @@ async function memberContentDigest(document) {
|
|
|
8524
8983
|
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
8525
8984
|
}
|
|
8526
8985
|
|
|
8986
|
+
// src/member-content-kinds.ts
|
|
8987
|
+
var MEMBER_PAGE_FEATURES = ["course", "share"];
|
|
8988
|
+
var MEMBER_PAGE_ICONS = [
|
|
8989
|
+
"home",
|
|
8990
|
+
"course",
|
|
8991
|
+
"library",
|
|
8992
|
+
"deals",
|
|
8993
|
+
"training",
|
|
8994
|
+
"events",
|
|
8995
|
+
"members",
|
|
8996
|
+
"share",
|
|
8997
|
+
"profile",
|
|
8998
|
+
"document",
|
|
8999
|
+
"calendar",
|
|
9000
|
+
"chat",
|
|
9001
|
+
"star"
|
|
9002
|
+
];
|
|
9003
|
+
var MEMBER_PAGE_TAGS = ["new"];
|
|
9004
|
+
function composeMemberSections(parent, chapter, pages) {
|
|
9005
|
+
const parentDeclared = new Map((parent?.sections ?? []).map((section) => [section.id, section]));
|
|
9006
|
+
const out = /* @__PURE__ */ new Map();
|
|
9007
|
+
for (const section of chapter?.sections ?? []) {
|
|
9008
|
+
const label = section.label ?? parentDeclared.get(section.id)?.label;
|
|
9009
|
+
out.set(section.id, { id: section.id, ...label ? { label } : {} });
|
|
9010
|
+
}
|
|
9011
|
+
for (const section of parent?.sections ?? []) if (!out.has(section.id)) out.set(section.id, section);
|
|
9012
|
+
const named = [...new Set(pages.map((page) => page.navigation.section))];
|
|
9013
|
+
for (const id7 of named.filter((id8) => !out.has(id8)).sort((a, b) => a.localeCompare(b))) out.set(id7, { id: id7, label: id7 });
|
|
9014
|
+
return [...out.values()].filter((section) => named.includes(section.id));
|
|
9015
|
+
}
|
|
9016
|
+
|
|
8527
9017
|
// src/member-content-validation-helpers.ts
|
|
8528
9018
|
var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
8529
9019
|
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8530
|
-
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
9020
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides", "sections"]);
|
|
8531
9021
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
8532
9022
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
8533
|
-
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
8534
|
-
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
9023
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell", "kind", "placeholder", "feature"]);
|
|
9024
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId", "tag", "icon"]);
|
|
8535
9025
|
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
8536
9026
|
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
8537
9027
|
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
@@ -8540,8 +9030,8 @@ var MAX_PAGES = 100;
|
|
|
8540
9030
|
var MAX_MARKDOWN2 = 65536;
|
|
8541
9031
|
var MAX_TEXT = 240;
|
|
8542
9032
|
var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8543
|
-
function unknownKeys2(value,
|
|
8544
|
-
for (const key3 of Object.keys(value)) if (!
|
|
9033
|
+
function unknownKeys2(value, allowed, path, errors) {
|
|
9034
|
+
for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8545
9035
|
}
|
|
8546
9036
|
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
8547
9037
|
if (value === void 0) {
|
|
@@ -8581,13 +9071,140 @@ function offerValue(value, path, errors, tiers, audience) {
|
|
|
8581
9071
|
};
|
|
8582
9072
|
}
|
|
8583
9073
|
|
|
9074
|
+
// src/member-content-validation-v2.ts
|
|
9075
|
+
var SECTION_KEYS = /* @__PURE__ */ new Set(["id", "label"]);
|
|
9076
|
+
var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set(["availableYear", "badge", "actionLabel"]);
|
|
9077
|
+
var FEATURE_KEYS = /* @__PURE__ */ new Set(["name", "ref"]);
|
|
9078
|
+
var MAX_SECTIONS = 24;
|
|
9079
|
+
var V2_ROOT_KEYS = ["sections"];
|
|
9080
|
+
var V2_PAGE_KEYS = ["kind", "placeholder", "feature"];
|
|
9081
|
+
var V2_NAV_KEYS = ["tag", "icon"];
|
|
9082
|
+
function rejectV2Fields(raw2, keys3, path, errors) {
|
|
9083
|
+
for (const key3 of keys3) if (raw2[key3] !== void 0) errors.push(`${path}.${key3} requires schemaVersion 2`);
|
|
9084
|
+
}
|
|
9085
|
+
function sectionsValue(value, errors) {
|
|
9086
|
+
if (value === void 0) return void 0;
|
|
9087
|
+
if (!Array.isArray(value)) {
|
|
9088
|
+
errors.push("document.sections must be an array");
|
|
9089
|
+
return void 0;
|
|
9090
|
+
}
|
|
9091
|
+
if (value.length > MAX_SECTIONS) errors.push(`document.sections supports at most ${MAX_SECTIONS} entries`);
|
|
9092
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9093
|
+
const sections = [];
|
|
9094
|
+
for (const [index2, item] of value.slice(0, MAX_SECTIONS).entries()) {
|
|
9095
|
+
const path = `document.sections[${index2}]`;
|
|
9096
|
+
const raw2 = record2(item);
|
|
9097
|
+
if (!raw2) {
|
|
9098
|
+
errors.push(`${path} must be an object`);
|
|
9099
|
+
continue;
|
|
9100
|
+
}
|
|
9101
|
+
unknownKeys2(raw2, SECTION_KEYS, path, errors);
|
|
9102
|
+
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
9103
|
+
if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9104
|
+
if (seen.has(id7)) errors.push(`${path}.id duplicates section "${id7}"`);
|
|
9105
|
+
seen.add(id7);
|
|
9106
|
+
const label = textValue(raw2.label, `${path}.label`, errors, 120);
|
|
9107
|
+
sections.push({ id: id7, ...label ? { label } : {} });
|
|
9108
|
+
}
|
|
9109
|
+
return sections;
|
|
9110
|
+
}
|
|
9111
|
+
var oneOf = (values, value) => typeof value === "string" && values.includes(value);
|
|
9112
|
+
function navigationV2(raw2, path, errors) {
|
|
9113
|
+
const out = {};
|
|
9114
|
+
if (raw2.tag !== void 0) {
|
|
9115
|
+
if (oneOf(MEMBER_PAGE_TAGS, raw2.tag)) out.tag = raw2.tag;
|
|
9116
|
+
else errors.push(`${path}.tag must be one of ${MEMBER_PAGE_TAGS.join(", ")}`);
|
|
9117
|
+
}
|
|
9118
|
+
if (raw2.icon !== void 0) {
|
|
9119
|
+
if (oneOf(MEMBER_PAGE_ICONS, raw2.icon)) out.icon = raw2.icon;
|
|
9120
|
+
else errors.push(`${path}.icon must be one of ${MEMBER_PAGE_ICONS.join(", ")}`);
|
|
9121
|
+
}
|
|
9122
|
+
return out;
|
|
9123
|
+
}
|
|
9124
|
+
function placeholderValue(value, path, errors) {
|
|
9125
|
+
const raw2 = record2(value);
|
|
9126
|
+
if (!raw2) {
|
|
9127
|
+
errors.push(`${path} must be an object`);
|
|
9128
|
+
return {};
|
|
9129
|
+
}
|
|
9130
|
+
unknownKeys2(raw2, PLACEHOLDER_KEYS, path, errors);
|
|
9131
|
+
const year = raw2.availableYear;
|
|
9132
|
+
if (year !== void 0 && (!Number.isInteger(year) || Number(year) < 2e3 || Number(year) > 2100)) {
|
|
9133
|
+
errors.push(`${path}.availableYear must be a year from 2000 to 2100`);
|
|
9134
|
+
}
|
|
9135
|
+
const badge = textValue(raw2.badge, `${path}.badge`, errors, 60);
|
|
9136
|
+
const actionLabel = textValue(raw2.actionLabel, `${path}.actionLabel`, errors, 80);
|
|
9137
|
+
return {
|
|
9138
|
+
...Number.isInteger(year) ? { availableYear: Number(year) } : {},
|
|
9139
|
+
...badge ? { badge } : {},
|
|
9140
|
+
...actionLabel ? { actionLabel } : {}
|
|
9141
|
+
};
|
|
9142
|
+
}
|
|
9143
|
+
function featureValue(value, path, errors) {
|
|
9144
|
+
const raw2 = record2(value);
|
|
9145
|
+
if (!raw2) {
|
|
9146
|
+
errors.push(`${path} must be an object`);
|
|
9147
|
+
return void 0;
|
|
9148
|
+
}
|
|
9149
|
+
unknownKeys2(raw2, FEATURE_KEYS, path, errors);
|
|
9150
|
+
if (!oneOf(MEMBER_PAGE_FEATURES, raw2.name)) {
|
|
9151
|
+
errors.push(`${path}.name must be one of ${MEMBER_PAGE_FEATURES.join(", ")}`);
|
|
9152
|
+
return void 0;
|
|
9153
|
+
}
|
|
9154
|
+
const ref = textValue(raw2.ref, `${path}.ref`, errors, 80);
|
|
9155
|
+
if (ref && !SLUG2.test(ref)) errors.push(`${path}.ref must be a lowercase slug`);
|
|
9156
|
+
return { name: raw2.name, ...ref ? { ref } : {} };
|
|
9157
|
+
}
|
|
9158
|
+
function pageKindValue(raw2, path, errors) {
|
|
9159
|
+
const kind = raw2.kind ?? "markdown";
|
|
9160
|
+
if (kind !== "markdown" && kind !== "placeholder" && kind !== "feature") {
|
|
9161
|
+
errors.push(`${path}.kind must be markdown, placeholder or feature`);
|
|
9162
|
+
return {};
|
|
9163
|
+
}
|
|
9164
|
+
if (kind !== "placeholder" && raw2.placeholder !== void 0) errors.push(`${path}.placeholder requires kind placeholder`);
|
|
9165
|
+
if (kind !== "feature" && raw2.feature !== void 0) errors.push(`${path}.feature requires kind feature`);
|
|
9166
|
+
if (kind === "markdown" && raw2.bodyMarkdown === void 0) errors.push(`${path}.bodyMarkdown is required`);
|
|
9167
|
+
if (kind === "feature" && raw2.bodyMarkdown !== void 0) errors.push(`${path}.bodyMarkdown is not supported on a feature page`);
|
|
9168
|
+
const out = raw2.kind === void 0 ? {} : { kind };
|
|
9169
|
+
if (kind === "placeholder") out.placeholder = placeholderValue(raw2.placeholder ?? {}, `${path}.placeholder`, errors);
|
|
9170
|
+
if (kind === "feature") {
|
|
9171
|
+
if (raw2.feature === void 0) errors.push(`${path}.feature is required for kind feature`);
|
|
9172
|
+
else {
|
|
9173
|
+
const feature = featureValue(raw2.feature, `${path}.feature`, errors);
|
|
9174
|
+
if (feature) out.feature = feature;
|
|
9175
|
+
}
|
|
9176
|
+
}
|
|
9177
|
+
return out;
|
|
9178
|
+
}
|
|
9179
|
+
function duplicateFeatures(pages, errors) {
|
|
9180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9181
|
+
for (const [index2, page] of pages.entries()) {
|
|
9182
|
+
if (!page.feature) continue;
|
|
9183
|
+
const key3 = `${page.feature.name}:${page.feature.ref ?? ""}`;
|
|
9184
|
+
if (seen.has(key3)) errors.push(`document.pages[${index2}].feature duplicates ${page.feature.name}${page.feature.ref ? ` "${page.feature.ref}"` : ""}`);
|
|
9185
|
+
seen.add(key3);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
9188
|
+
function sectionReferences(pages, sections, parentPages, errors) {
|
|
9189
|
+
if (!sections) return;
|
|
9190
|
+
const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), ...parentPages.map((page) => page.navigation.section)]);
|
|
9191
|
+
for (const [index2, page] of pages.entries()) {
|
|
9192
|
+
if (!known.has(page.navigation.section)) {
|
|
9193
|
+
errors.push(`document.pages[${index2}].navigation.section references undeclared section "${page.navigation.section}"`);
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
}
|
|
9197
|
+
|
|
8584
9198
|
// src/member-content-validation.ts
|
|
8585
9199
|
function validateMemberContentDocument(input, options = {}) {
|
|
8586
9200
|
const errors = [];
|
|
8587
9201
|
const root = record2(input);
|
|
8588
9202
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
8589
9203
|
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
8590
|
-
if (root.schemaVersion !==
|
|
9204
|
+
if (root.schemaVersion !== 1 && root.schemaVersion !== 2) errors.push("document.schemaVersion must be 1 or 2");
|
|
9205
|
+
const v2 = root.schemaVersion === 2;
|
|
9206
|
+
if (!v2) rejectV2Fields(root, V2_ROOT_KEYS, "document", errors);
|
|
9207
|
+
const sections = v2 ? sectionsValue(root.sections, errors) : void 0;
|
|
8591
9208
|
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
8592
9209
|
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
8593
9210
|
const sourceRaw = record2(root.source);
|
|
@@ -8632,6 +9249,8 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8632
9249
|
continue;
|
|
8633
9250
|
}
|
|
8634
9251
|
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
9252
|
+
if (!v2) rejectV2Fields(raw2, V2_PAGE_KEYS, path, errors);
|
|
9253
|
+
const kindFields = v2 ? pageKindValue(raw2, path, errors) : {};
|
|
8635
9254
|
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
8636
9255
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
8637
9256
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
@@ -8646,13 +9265,19 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8646
9265
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
8647
9266
|
else {
|
|
8648
9267
|
unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
9268
|
+
if (!v2) rejectV2Fields(navRaw, V2_NAV_KEYS, `${path}.navigation`, errors);
|
|
8649
9269
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
8650
9270
|
const order2 = navRaw.order;
|
|
8651
9271
|
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
8652
9272
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
8653
9273
|
}
|
|
8654
9274
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
8655
|
-
navigation = {
|
|
9275
|
+
navigation = {
|
|
9276
|
+
section,
|
|
9277
|
+
order: Number.isInteger(order2) ? Number(order2) : index2,
|
|
9278
|
+
...parentId ? { parentId } : {},
|
|
9279
|
+
...v2 ? navigationV2(navRaw, `${path}.navigation`, errors) : {}
|
|
9280
|
+
};
|
|
8656
9281
|
}
|
|
8657
9282
|
const audienceRaw = record2(raw2.audience);
|
|
8658
9283
|
let audience = { kind: "all-members" };
|
|
@@ -8678,7 +9303,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8678
9303
|
}
|
|
8679
9304
|
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
8680
9305
|
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
|
|
8681
|
-
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2,
|
|
9306
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, !v2);
|
|
8682
9307
|
let upsell;
|
|
8683
9308
|
if (raw2.upsell !== void 0) {
|
|
8684
9309
|
const upsellRaw = record2(raw2.upsell);
|
|
@@ -8695,7 +9320,22 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8695
9320
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
8696
9321
|
}
|
|
8697
9322
|
}
|
|
8698
|
-
pages.push({
|
|
9323
|
+
pages.push({
|
|
9324
|
+
id: id7,
|
|
9325
|
+
slug,
|
|
9326
|
+
title,
|
|
9327
|
+
...summary ? { summary } : {},
|
|
9328
|
+
navigation,
|
|
9329
|
+
audience,
|
|
9330
|
+
...teaserMarkdown ? { teaserMarkdown } : {},
|
|
9331
|
+
...bodyMarkdown !== void 0 ? { bodyMarkdown } : {},
|
|
9332
|
+
...upsell ? { upsell } : {},
|
|
9333
|
+
...kindFields
|
|
9334
|
+
});
|
|
9335
|
+
}
|
|
9336
|
+
if (v2) {
|
|
9337
|
+
duplicateFeatures(pages, errors);
|
|
9338
|
+
sectionReferences(pages, sections, options.parentPages ?? [], errors);
|
|
8699
9339
|
}
|
|
8700
9340
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
8701
9341
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -8742,21 +9382,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
8742
9382
|
}
|
|
8743
9383
|
if (errors.length) return { ok: false, errors };
|
|
8744
9384
|
return { ok: true, value: {
|
|
8745
|
-
schemaVersion:
|
|
9385
|
+
schemaVersion: v2 ? 2 : 1,
|
|
8746
9386
|
revision: Number(root.revision),
|
|
8747
9387
|
generatedAt: Number(root.generatedAt),
|
|
8748
9388
|
source,
|
|
8749
9389
|
...target ? { target } : {},
|
|
8750
9390
|
pages,
|
|
8751
|
-
...upsellOverrides ? { upsellOverrides } : {}
|
|
9391
|
+
...upsellOverrides ? { upsellOverrides } : {},
|
|
9392
|
+
...sections ? { sections } : {}
|
|
8752
9393
|
} };
|
|
8753
9394
|
}
|
|
8754
9395
|
|
|
8755
9396
|
// src/member-content.ts
|
|
8756
|
-
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
8757
9397
|
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
8758
9398
|
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
8759
|
-
var pageOrder = (a, b) => a.navigation.section.
|
|
9399
|
+
var pageOrder = (rank) => (a, b) => (rank.get(a.navigation.section) ?? 0) - (rank.get(b.navigation.section) ?? 0) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
|
|
8760
9400
|
function composeMemberContent(parent, chapter) {
|
|
8761
9401
|
const pages = [];
|
|
8762
9402
|
const parentPages = /* @__PURE__ */ new Map();
|
|
@@ -8775,20 +9415,24 @@ function composeMemberContent(parent, chapter) {
|
|
|
8775
9415
|
}
|
|
8776
9416
|
pages.push({ ...page, source: chapter.source });
|
|
8777
9417
|
}
|
|
9418
|
+
const sections = composeMemberSections(parent, chapter, pages);
|
|
9419
|
+
const rank = new Map(sections.map((section, index2) => [section.id, index2]));
|
|
8778
9420
|
return {
|
|
8779
|
-
pages: pages.sort(pageOrder),
|
|
9421
|
+
pages: pages.sort(pageOrder(rank)),
|
|
9422
|
+
sections,
|
|
8780
9423
|
parentRevision: parent?.revision ?? null,
|
|
8781
9424
|
chapterRevision: chapter?.revision ?? null
|
|
8782
9425
|
};
|
|
8783
9426
|
}
|
|
8784
|
-
var
|
|
9427
|
+
var memberPageAllowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
8785
9428
|
function projectMemberContent(content, viewer, tiers) {
|
|
8786
9429
|
const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
8787
9430
|
return {
|
|
8788
9431
|
viewer: { ...viewer },
|
|
8789
9432
|
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
9433
|
+
sections: content.sections ?? composeMemberSections(null, null, content.pages),
|
|
8790
9434
|
pages: content.pages.map((page) => {
|
|
8791
|
-
const canRead =
|
|
9435
|
+
const canRead = memberPageAllowed(page, viewer);
|
|
8792
9436
|
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
8793
9437
|
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
8794
9438
|
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
@@ -8801,8 +9445,11 @@ function projectMemberContent(content, viewer, tiers) {
|
|
|
8801
9445
|
audience: page.audience,
|
|
8802
9446
|
source: page.source,
|
|
8803
9447
|
access: canRead ? "allowed" : "locked",
|
|
9448
|
+
kind: page.kind ?? "markdown",
|
|
9449
|
+
...page.placeholder ? { placeholder: page.placeholder } : {},
|
|
9450
|
+
...page.feature ? { feature: page.feature } : {},
|
|
8804
9451
|
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
8805
|
-
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
9452
|
+
...canRead && page.bodyMarkdown !== void 0 ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
8806
9453
|
...upsell ? { upsell } : {}
|
|
8807
9454
|
};
|
|
8808
9455
|
})
|
|
@@ -8814,33 +9461,33 @@ var control = (row2) => row2 && row2.document && typeof row2.document === "objec
|
|
|
8814
9461
|
var local2 = (row2) => row2 && row2.document && typeof row2.document === "object" ? row2 : null;
|
|
8815
9462
|
async function exists(db, ns, where) {
|
|
8816
9463
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
8817
|
-
const
|
|
8818
|
-
return
|
|
9464
|
+
const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
9465
|
+
return rows6.some((row2) => Object.entries(where).every(([key3, value]) => row2[key3] === value));
|
|
8819
9466
|
}
|
|
8820
9467
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
8821
9468
|
const where = { targetId, environment, active: true };
|
|
8822
9469
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
8823
|
-
const
|
|
9470
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
8824
9471
|
where,
|
|
8825
9472
|
order: { revision: "desc" },
|
|
8826
9473
|
limit: 1
|
|
8827
9474
|
} } })).memberContentControls;
|
|
8828
|
-
return control(Array.isArray(
|
|
9475
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8829
9476
|
}
|
|
8830
9477
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
8831
9478
|
const where = { targetId, environment };
|
|
8832
9479
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
8833
|
-
const
|
|
9480
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
8834
9481
|
where,
|
|
8835
9482
|
order: { revision: "desc" },
|
|
8836
9483
|
limit: 1
|
|
8837
9484
|
} } })).memberContentControls;
|
|
8838
|
-
return control(Array.isArray(
|
|
9485
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8839
9486
|
}
|
|
8840
9487
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
8841
9488
|
const id7 = `member-content-local:${environment}`;
|
|
8842
|
-
const
|
|
8843
|
-
return local2(Array.isArray(
|
|
9489
|
+
const rows6 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
|
|
9490
|
+
return local2(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
8844
9491
|
}
|
|
8845
9492
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
8846
9493
|
const { document, digest } = envelope;
|
|
@@ -8887,7 +9534,8 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
|
|
|
8887
9534
|
var ADMIN_PATH = "/api/admin/member-content";
|
|
8888
9535
|
var BODY_CAP = 1e6;
|
|
8889
9536
|
var reject4 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
|
|
8890
|
-
async function groupAndTiers(db, chapterId) {
|
|
9537
|
+
async function groupAndTiers(db, chapterId, mode = "chapter") {
|
|
9538
|
+
if (mode === "hub") return { group: null, tiers: [] };
|
|
8891
9539
|
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
8892
9540
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
8893
9541
|
if (!group) return { group: null, tiers: [] };
|
|
@@ -8924,7 +9572,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
8924
9572
|
if (document.source.appId !== settings.sourceId) return reject4("wrong_source", 403);
|
|
8925
9573
|
if (document.target?.chapterId !== ctx.chapter.id) return reject4("wrong_target", 409);
|
|
8926
9574
|
if (document.target.environment !== env.ODLA_ENV) return reject4("wrong_environment", 409);
|
|
8927
|
-
const { tiers } = await groupAndTiers(db, ctx.chapter.id);
|
|
9575
|
+
const { tiers } = await groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode);
|
|
8928
9576
|
const checked = validateMemberContentDocument(document, {
|
|
8929
9577
|
sourceType: "parent",
|
|
8930
9578
|
sourceAppId: settings.sourceId,
|
|
@@ -8978,17 +9626,13 @@ var handleMemberContent = async (req, url, env, ctx) => {
|
|
|
8978
9626
|
const user = await ctx.verifyUser(req, env);
|
|
8979
9627
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
8980
9628
|
const db = ctx.makeDb(env);
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
tierId = typeof member.source.tierId === "string" ? member.source.tierId : null;
|
|
8986
|
-
} catch {
|
|
8987
|
-
return json({ error: "membership access unavailable" }, 503);
|
|
8988
|
-
}
|
|
9629
|
+
const resolved = await resolvePortalMember(db, env, ctx, user);
|
|
9630
|
+
if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
|
|
9631
|
+
if (resolved.status !== "member") return json({ error: "member access required" }, 403);
|
|
9632
|
+
const tierId = resolved.member.tierId;
|
|
8989
9633
|
const [{ parent, chapter }, { group, tiers }] = await Promise.all([
|
|
8990
9634
|
activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
|
|
8991
|
-
groupAndTiers(db, ctx.chapter.id)
|
|
9635
|
+
groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
|
|
8992
9636
|
]);
|
|
8993
9637
|
const effective = composeMemberContent(parent?.document, chapter?.document);
|
|
8994
9638
|
let offerable = tiers;
|
|
@@ -9007,7 +9651,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
9007
9651
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
9008
9652
|
const [{ parent, chapter }, { tiers }] = await Promise.all([
|
|
9009
9653
|
activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
|
|
9010
|
-
groupAndTiers(db, ctx.chapter.id)
|
|
9654
|
+
groupAndTiers(db, ctx.chapter.id, ctx.chapter.mode)
|
|
9011
9655
|
]);
|
|
9012
9656
|
if (req.method === "GET") {
|
|
9013
9657
|
const effective = composeMemberContent(parent?.document, chapter?.document);
|
|
@@ -9015,7 +9659,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
9015
9659
|
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
9016
9660
|
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
9017
9661
|
effective,
|
|
9018
|
-
tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
|
|
9662
|
+
tiers: tiers.map(({ id: id7, name: name2, priceCents, active }) => ({ id: id7, name: name2, priceCents, active })),
|
|
9019
9663
|
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
9020
9664
|
});
|
|
9021
9665
|
}
|
|
@@ -9042,6 +9686,8 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
9042
9686
|
parentPages: parent?.document.pages ?? []
|
|
9043
9687
|
});
|
|
9044
9688
|
if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
|
|
9689
|
+
const tiered = ctx.chapter.mode === "hub" ? checked.value.pages.findIndex((page) => page.audience.kind === "tiers") : -1;
|
|
9690
|
+
if (tiered >= 0) return json({ error: "invalid member content", errors: [`document.pages[${tiered}].audience must be all-members: a hub has no tiers`] }, 400);
|
|
9045
9691
|
try {
|
|
9046
9692
|
const saved = await saveLocalMemberContent(
|
|
9047
9693
|
db,
|
|
@@ -9063,13 +9709,521 @@ function withLocalAuthority(value, revision, generatedAt, appId) {
|
|
|
9063
9709
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
9064
9710
|
return {
|
|
9065
9711
|
...value,
|
|
9066
|
-
|
|
9712
|
+
// The author chooses the contract version; anything else defaults to 1.
|
|
9713
|
+
schemaVersion: value.schemaVersion === 2 ? 2 : 1,
|
|
9067
9714
|
revision,
|
|
9068
9715
|
generatedAt,
|
|
9069
9716
|
source: { type: "chapter", appId }
|
|
9070
9717
|
};
|
|
9071
9718
|
}
|
|
9072
9719
|
|
|
9720
|
+
// src/portal-http.ts
|
|
9721
|
+
async function readJsonObject(req, maxBytes) {
|
|
9722
|
+
const text11 = await readBoundedRequestText(req, maxBytes);
|
|
9723
|
+
if (text11 instanceof Response) return text11;
|
|
9724
|
+
try {
|
|
9725
|
+
const value = JSON.parse(text11);
|
|
9726
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
9727
|
+
} catch {
|
|
9728
|
+
}
|
|
9729
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
9730
|
+
}
|
|
9731
|
+
|
|
9732
|
+
// src/worker-routes-hub-members.ts
|
|
9733
|
+
var HUB_MEMBERS_PATH = "/api/admin/hub-members";
|
|
9734
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9735
|
+
async function grantId(emailNormalized) {
|
|
9736
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(emailNormalized));
|
|
9737
|
+
return `hub-member:${Array.from(new Uint8Array(digest).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
9738
|
+
}
|
|
9739
|
+
var view = (row2) => ({
|
|
9740
|
+
id: row2.id,
|
|
9741
|
+
email: row2.emailNormalized,
|
|
9742
|
+
firstName: row2.firstName ?? null,
|
|
9743
|
+
lastName: row2.lastName ?? null,
|
|
9744
|
+
status: row2.status,
|
|
9745
|
+
grantedBy: row2.grantedBy,
|
|
9746
|
+
grantedAt: row2.grantedAt,
|
|
9747
|
+
revokedBy: row2.revokedBy ?? null,
|
|
9748
|
+
revokedAt: row2.revokedAt ?? null,
|
|
9749
|
+
revision: row2.revision
|
|
9750
|
+
});
|
|
9751
|
+
var name = (value) => typeof value === "string" && value.trim() ? value.trim().slice(0, 80) : void 0;
|
|
9752
|
+
var handleAdminHubMembers = async (req, url, env, ctx) => {
|
|
9753
|
+
if (url.pathname !== HUB_MEMBERS_PATH && !url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) return null;
|
|
9754
|
+
if (ctx.chapter.mode !== "hub" || !servesMembers(ctx)) return json({ error: "not found" }, 404);
|
|
9755
|
+
const user = await ctx.verifyUser(req, env);
|
|
9756
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
9757
|
+
const db = ctx.makeDb(env);
|
|
9758
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
9759
|
+
const actor = user.email ?? user.userId;
|
|
9760
|
+
if (req.method === "GET" && url.pathname === HUB_MEMBERS_PATH) {
|
|
9761
|
+
const rows6 = (await db.query({ hubMemberGrants: { $: { limit: 1e3 } } })).hubMemberGrants ?? [];
|
|
9762
|
+
return json({ members: rows6.map(view).sort((a, b) => Number(b.grantedAt) - Number(a.grantedAt)) });
|
|
9763
|
+
}
|
|
9764
|
+
if (req.method === "POST" && url.pathname === HUB_MEMBERS_PATH) {
|
|
9765
|
+
const body = await readJsonObject(req, 8192);
|
|
9766
|
+
if (body instanceof Response) return body;
|
|
9767
|
+
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
9768
|
+
if (!EMAIL.test(email)) return json({ error: "a valid email is required" }, 400);
|
|
9769
|
+
const emailNormalized = normalizeChapterEmail(email);
|
|
9770
|
+
const id7 = await grantId(emailNormalized);
|
|
9771
|
+
const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
|
|
9772
|
+
if (current?.status === "active") return json({ member: view(current), status: "already-active" });
|
|
9773
|
+
const revision = Number(current?.revision ?? 0) + 1;
|
|
9774
|
+
const attrs = {
|
|
9775
|
+
id: id7,
|
|
9776
|
+
emailNormalized,
|
|
9777
|
+
status: "active",
|
|
9778
|
+
grantedBy: actor,
|
|
9779
|
+
grantedAt: Date.now(),
|
|
9780
|
+
revision,
|
|
9781
|
+
...name(body.firstName) ? { firstName: name(body.firstName) } : {},
|
|
9782
|
+
...name(body.lastName) ? { lastName: name(body.lastName) } : {}
|
|
9783
|
+
};
|
|
9784
|
+
const ops = [{ t: "update", ns: "hubMemberGrants", id: id7, attrs }];
|
|
9785
|
+
if (current) ops.push({ t: "retract", ns: "hubMemberGrants", id: id7, attrs: ["revokedBy", "revokedAt"] });
|
|
9786
|
+
try {
|
|
9787
|
+
await db.transact(ops, { mutationId: `hub-member-grant:${id7}:${revision}`, guards: [current ? { ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } } : { ns: "hubMemberGrants", id: id7, exists: false }] });
|
|
9788
|
+
} catch {
|
|
9789
|
+
return json({ error: "the roster changed; reload and try again" }, 409);
|
|
9790
|
+
}
|
|
9791
|
+
return json({ member: view({ ...current, ...attrs, revokedBy: void 0, revokedAt: void 0 }), status: current ? "re-granted" : "granted" }, current ? 200 : 201);
|
|
9792
|
+
}
|
|
9793
|
+
if (req.method === "DELETE" && url.pathname.startsWith(`${HUB_MEMBERS_PATH}/`)) {
|
|
9794
|
+
const id7 = decodeURIComponent(url.pathname.slice(HUB_MEMBERS_PATH.length + 1));
|
|
9795
|
+
const current = (await db.query({ hubMemberGrants: { $: { where: { id: id7 }, limit: 1 } } })).hubMemberGrants?.[0];
|
|
9796
|
+
if (!current) return json({ error: "not found" }, 404);
|
|
9797
|
+
if (current.status !== "active") return json({ member: view(current), status: "already-revoked" });
|
|
9798
|
+
const attrs = { status: "revoked", revokedBy: actor, revokedAt: Date.now(), revision: Number(current.revision) + 1 };
|
|
9799
|
+
try {
|
|
9800
|
+
await db.transact([{ t: "update", ns: "hubMemberGrants", id: id7, attrs }], {
|
|
9801
|
+
mutationId: `hub-member-revoke:${id7}:${attrs.revision}`,
|
|
9802
|
+
guards: [{ ns: "hubMemberGrants", id: id7, exists: true, equals: { revision: current.revision } }]
|
|
9803
|
+
});
|
|
9804
|
+
} catch {
|
|
9805
|
+
return json({ error: "the roster changed; reload and try again" }, 409);
|
|
9806
|
+
}
|
|
9807
|
+
return json({ member: view({ ...current, ...attrs }), status: "revoked" });
|
|
9808
|
+
}
|
|
9809
|
+
return json({ error: "method not allowed" }, 405);
|
|
9810
|
+
};
|
|
9811
|
+
|
|
9812
|
+
// src/image-metadata.ts
|
|
9813
|
+
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
9814
|
+
var PNG_KEEP = /* @__PURE__ */ new Set(["IHDR", "PLTE", "IDAT", "IEND", "tRNS", "gAMA", "cHRM", "sRGB", "sBIT", "bKGD", "pHYs"]);
|
|
9815
|
+
var JPEG_KEEP = (marker) => marker === 224 || marker === 238 || marker === 219 || marker === 196 || marker === 221 || marker >= 192 && marker <= 207 && marker !== 200;
|
|
9816
|
+
var u16 = (b, i) => b[i] << 8 | b[i + 1];
|
|
9817
|
+
var u32 = (b, i) => (b[i] << 24 >>> 0) + (b[i + 1] << 16) + (b[i + 2] << 8) + b[i + 3];
|
|
9818
|
+
function concat(parts) {
|
|
9819
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
9820
|
+
let offset = 0;
|
|
9821
|
+
for (const part of parts) {
|
|
9822
|
+
out.set(part, offset);
|
|
9823
|
+
offset += part.length;
|
|
9824
|
+
}
|
|
9825
|
+
return out;
|
|
9826
|
+
}
|
|
9827
|
+
function cleanPng(bytes) {
|
|
9828
|
+
const parts = [bytes.subarray(0, 8)];
|
|
9829
|
+
let width = 0, height = 0, offset = 8, ended = false;
|
|
9830
|
+
while (offset + 12 <= bytes.length) {
|
|
9831
|
+
const length = u32(bytes, offset);
|
|
9832
|
+
const end = offset + 12 + length;
|
|
9833
|
+
if (end > bytes.length) return null;
|
|
9834
|
+
const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
|
|
9835
|
+
if (type === "IHDR") {
|
|
9836
|
+
width = u32(bytes, offset + 8);
|
|
9837
|
+
height = u32(bytes, offset + 12);
|
|
9838
|
+
}
|
|
9839
|
+
if (PNG_KEEP.has(type)) parts.push(bytes.subarray(offset, end));
|
|
9840
|
+
offset = end;
|
|
9841
|
+
if (type === "IEND") {
|
|
9842
|
+
ended = true;
|
|
9843
|
+
break;
|
|
9844
|
+
}
|
|
9845
|
+
}
|
|
9846
|
+
if (!ended || !width || !height) return null;
|
|
9847
|
+
return { contentType: "image/png", extension: "png", width, height, bytes: concat(parts) };
|
|
9848
|
+
}
|
|
9849
|
+
function cleanJpeg(bytes) {
|
|
9850
|
+
const parts = [bytes.subarray(0, 2)];
|
|
9851
|
+
let width = 0, height = 0, offset = 2;
|
|
9852
|
+
while (offset + 4 <= bytes.length) {
|
|
9853
|
+
if (bytes[offset] !== 255) return null;
|
|
9854
|
+
const marker = bytes[offset + 1];
|
|
9855
|
+
if (marker === 255) {
|
|
9856
|
+
offset += 1;
|
|
9857
|
+
continue;
|
|
9858
|
+
}
|
|
9859
|
+
const length = u16(bytes, offset + 2);
|
|
9860
|
+
const end = offset + 2 + length;
|
|
9861
|
+
if (length < 2 || end > bytes.length) return null;
|
|
9862
|
+
if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
|
|
9863
|
+
height = u16(bytes, offset + 5);
|
|
9864
|
+
width = u16(bytes, offset + 7);
|
|
9865
|
+
}
|
|
9866
|
+
if (marker === 218) {
|
|
9867
|
+
parts.push(bytes.subarray(offset));
|
|
9868
|
+
return width && height ? { contentType: "image/jpeg", extension: "jpg", width, height, bytes: concat(parts) } : null;
|
|
9869
|
+
}
|
|
9870
|
+
if (JPEG_KEEP(marker)) parts.push(bytes.subarray(offset, end));
|
|
9871
|
+
offset = end;
|
|
9872
|
+
}
|
|
9873
|
+
return null;
|
|
9874
|
+
}
|
|
9875
|
+
function cleanImage(bytes) {
|
|
9876
|
+
if (bytes.length > 8 && PNG_SIGNATURE.every((byte, index2) => bytes[index2] === byte)) return cleanPng(bytes);
|
|
9877
|
+
if (bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return cleanJpeg(bytes);
|
|
9878
|
+
return null;
|
|
9879
|
+
}
|
|
9880
|
+
|
|
9881
|
+
// src/member-profile.ts
|
|
9882
|
+
var MEMBER_PROFILE_FIELDS = {
|
|
9883
|
+
preferredName: "person.preferredName",
|
|
9884
|
+
location: "person.region",
|
|
9885
|
+
prompts: "person.profilePrompts",
|
|
9886
|
+
introducedByText: "person.introducedByText"
|
|
9887
|
+
};
|
|
9888
|
+
var MAX_PROMPT_ANSWER = 280;
|
|
9889
|
+
var clean = (value) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
|
|
9890
|
+
function optionalText(body, key3, max, errors) {
|
|
9891
|
+
if (!(key3 in body)) return void 0;
|
|
9892
|
+
const value = body[key3];
|
|
9893
|
+
if (value === null || value === "") return null;
|
|
9894
|
+
if (typeof value !== "string") {
|
|
9895
|
+
errors.push(`${key3} must be a string`);
|
|
9896
|
+
return void 0;
|
|
9897
|
+
}
|
|
9898
|
+
const text11 = clean(value);
|
|
9899
|
+
if (text11.length > max) errors.push(`${key3} must be at most ${max} characters`);
|
|
9900
|
+
return text11 || null;
|
|
9901
|
+
}
|
|
9902
|
+
function validateProfilePatch(body, portal) {
|
|
9903
|
+
const errors = [];
|
|
9904
|
+
for (const key3 of Object.keys(body)) {
|
|
9905
|
+
if (key3 === "expectedRevision") continue;
|
|
9906
|
+
if (!(key3 in MEMBER_PROFILE_FIELDS)) errors.push(`${key3} is not an editable profile field`);
|
|
9907
|
+
else if (!MEMBER_SELF_EDITABLE_FIELDS.has(MEMBER_PROFILE_FIELDS[key3])) errors.push(`${key3} is read-only`);
|
|
9908
|
+
}
|
|
9909
|
+
const patch = {};
|
|
9910
|
+
const preferredName = optionalText(body, "preferredName", 80, errors);
|
|
9911
|
+
if (preferredName !== void 0) patch.preferredName = preferredName;
|
|
9912
|
+
const location = optionalText(body, "location", 80, errors);
|
|
9913
|
+
if (location !== void 0) patch.location = location;
|
|
9914
|
+
const introducedByText = optionalText(body, "introducedByText", 120, errors);
|
|
9915
|
+
if (introducedByText !== void 0) patch.introducedByText = introducedByText;
|
|
9916
|
+
if ("prompts" in body) {
|
|
9917
|
+
const raw2 = body.prompts;
|
|
9918
|
+
if (!Array.isArray(raw2)) errors.push("prompts must be an array");
|
|
9919
|
+
else {
|
|
9920
|
+
if (raw2.length > portal.maxPrompts) errors.push(`prompts supports at most ${portal.maxPrompts} answers`);
|
|
9921
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9922
|
+
const prompts = [];
|
|
9923
|
+
for (const [index2, item] of raw2.entries()) {
|
|
9924
|
+
const entry = item && typeof item === "object" ? item : {};
|
|
9925
|
+
const prompt = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
|
|
9926
|
+
const answer = typeof entry.answer === "string" ? clean(entry.answer) : "";
|
|
9927
|
+
if (!portal.profilePrompts.includes(prompt)) errors.push(`prompts[${index2}].prompt is not one of this site's prompts`);
|
|
9928
|
+
if (seen.has(prompt)) errors.push(`prompts[${index2}].prompt is already answered`);
|
|
9929
|
+
if (entry.answer !== void 0 && typeof entry.answer !== "string") errors.push(`prompts[${index2}].answer must be a string`);
|
|
9930
|
+
if (answer.length > MAX_PROMPT_ANSWER) errors.push(`prompts[${index2}].answer must be at most ${MAX_PROMPT_ANSWER} characters`);
|
|
9931
|
+
seen.add(prompt);
|
|
9932
|
+
prompts.push({ prompt, answer });
|
|
9933
|
+
}
|
|
9934
|
+
patch.prompts = prompts;
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
return errors.length ? { ok: false, errors } : { ok: true, patch };
|
|
9938
|
+
}
|
|
9939
|
+
var nameKey = (value) => value.toLowerCase().normalize("NFKC").replace(/[^\p{L}\p{N}\s'-]/gu, " ").replace(/\s+/g, " ").trim();
|
|
9940
|
+
|
|
9941
|
+
// src/member-profile-store.ts
|
|
9942
|
+
async function loadMemberProfile(db, memberId) {
|
|
9943
|
+
return (await db.query({ memberProfiles: { $: { where: { id: memberId }, limit: 1 } } })).memberProfiles?.[0] ?? null;
|
|
9944
|
+
}
|
|
9945
|
+
async function membersNamed(db, mode, chapterId, text11, selfId) {
|
|
9946
|
+
const wanted = nameKey(text11);
|
|
9947
|
+
if (!wanted) return [];
|
|
9948
|
+
const rows6 = mode === "hub" ? (await db.query({ hubMemberGrants: { $: { where: { status: "active" }, limit: 5e3 } } })).hubMemberGrants ?? [] : ((await db.query({ applications: { $: { where: { groupId: chapterId, status: "approved" }, limit: 5e3 } } })).applications ?? []).filter((row2) => row2.canceled !== true);
|
|
9949
|
+
return rows6.filter((row2) => String(row2.id) !== selfId && nameKey(`${row2.firstName ?? ""} ${row2.lastName ?? ""}`) === wanted).map((row2) => String(row2.id));
|
|
9950
|
+
}
|
|
9951
|
+
function reviewItemOp(id7, kind, detail, memberId, relatedMemberId) {
|
|
9952
|
+
return { t: "update", ns: "portalReviewItems", id: id7, attrs: {
|
|
9953
|
+
id: id7,
|
|
9954
|
+
kind,
|
|
9955
|
+
detail,
|
|
9956
|
+
status: "open",
|
|
9957
|
+
createdAt: Date.now(),
|
|
9958
|
+
...memberId ? { memberId } : {},
|
|
9959
|
+
...relatedMemberId ? { relatedMemberId } : {}
|
|
9960
|
+
} };
|
|
9961
|
+
}
|
|
9962
|
+
async function saveMemberProfile(db, input) {
|
|
9963
|
+
const current = await loadMemberProfile(db, input.memberId);
|
|
9964
|
+
if (Number(current?.revision ?? 0) !== input.expectedRevision) throw new Error("member_profile_revision_conflict");
|
|
9965
|
+
const now = Date.now();
|
|
9966
|
+
const revision = input.expectedRevision + 1;
|
|
9967
|
+
const attrs = { id: input.memberId, memberId: input.memberId, revision, updatedAt: now };
|
|
9968
|
+
const retract = [];
|
|
9969
|
+
const set = (key3, value) => {
|
|
9970
|
+
if (value === null) retract.push(key3);
|
|
9971
|
+
else attrs[key3] = value;
|
|
9972
|
+
};
|
|
9973
|
+
if (input.patch.preferredName !== void 0) set("preferredName", input.patch.preferredName);
|
|
9974
|
+
if (input.patch.location !== void 0) set("location", input.patch.location);
|
|
9975
|
+
if (input.patch.prompts !== void 0) attrs.prompts = input.patch.prompts;
|
|
9976
|
+
const ops = [];
|
|
9977
|
+
let review = null;
|
|
9978
|
+
const introduced = input.patch.introducedByText;
|
|
9979
|
+
if (introduced !== void 0 && introduced !== (current?.introducedByText ?? null)) {
|
|
9980
|
+
set("introducedByText", introduced);
|
|
9981
|
+
if (introduced === null) retract.push("introducedByMemberId", "introducedAt");
|
|
9982
|
+
else {
|
|
9983
|
+
const matches2 = await membersNamed(db, input.mode, input.chapterId, introduced, input.memberId);
|
|
9984
|
+
if (matches2.length === 1) {
|
|
9985
|
+
Object.assign(attrs, { introducedByMemberId: matches2[0], introducedAt: now });
|
|
9986
|
+
review = "matched";
|
|
9987
|
+
} else {
|
|
9988
|
+
retract.push("introducedByMemberId", "introducedAt");
|
|
9989
|
+
ops.push(reviewItemOp(
|
|
9990
|
+
`introduction:${input.memberId}:${revision}`,
|
|
9991
|
+
"introduction_unmatched",
|
|
9992
|
+
{ text: introduced, candidates: matches2.length },
|
|
9993
|
+
input.memberId
|
|
9994
|
+
));
|
|
9995
|
+
review = "queued";
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
}
|
|
9999
|
+
ops.unshift({ t: "update", ns: "memberProfiles", id: input.memberId, attrs });
|
|
10000
|
+
const absent = retract.filter((key3) => current && current[key3] !== void 0);
|
|
10001
|
+
if (absent.length) ops.splice(1, 0, { t: "retract", ns: "memberProfiles", id: input.memberId, attrs: absent });
|
|
10002
|
+
await db.transact(ops, {
|
|
10003
|
+
mutationId: `member-profile:${input.memberId}:${revision}`,
|
|
10004
|
+
guards: [current ? { ns: "memberProfiles", id: input.memberId, exists: true, equals: { revision: input.expectedRevision } } : { ns: "memberProfiles", id: input.memberId, exists: false }]
|
|
10005
|
+
});
|
|
10006
|
+
const row2 = { ...current, ...attrs };
|
|
10007
|
+
for (const key3 of absent) delete row2[key3];
|
|
10008
|
+
return { row: row2, review };
|
|
10009
|
+
}
|
|
10010
|
+
|
|
10011
|
+
// src/worker-routes-member-profile.ts
|
|
10012
|
+
var MEMBER_PROFILE_PATH = "/api/member/profile";
|
|
10013
|
+
var MEMBER_PHOTO_PATH = "/api/member/profile/photo";
|
|
10014
|
+
var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
|
|
10015
|
+
var PHOTO_SIZE = 400;
|
|
10016
|
+
async function portalMemberGate(req, env, ctx) {
|
|
10017
|
+
const user = await ctx.verifyUser(req, env);
|
|
10018
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10019
|
+
const db = ctx.makeDb(env);
|
|
10020
|
+
const resolved = await resolvePortalMember(db, env, ctx, user);
|
|
10021
|
+
if (resolved.status === "unavailable") return json({ error: "membership access unavailable" }, 503);
|
|
10022
|
+
if (resolved.status !== "member") return json({ error: "member access required" }, 403);
|
|
10023
|
+
return { db, member: resolved.member };
|
|
10024
|
+
}
|
|
10025
|
+
async function profileView(db, ctx, member) {
|
|
10026
|
+
const row2 = await loadMemberProfile(db, member.memberId);
|
|
10027
|
+
const portal = ctx.chapter.memberPortal;
|
|
10028
|
+
return {
|
|
10029
|
+
revision: Number(row2?.revision ?? 0),
|
|
10030
|
+
identity: await portalIdentity(db, ctx, member),
|
|
10031
|
+
profile: {
|
|
10032
|
+
preferredName: row2?.preferredName ?? null,
|
|
10033
|
+
location: row2?.location ?? null,
|
|
10034
|
+
prompts: Array.isArray(row2?.prompts) ? row2.prompts : [],
|
|
10035
|
+
introducedByText: row2?.introducedByText ?? null,
|
|
10036
|
+
introducedByMatched: typeof row2?.introducedByMemberId === "string"
|
|
10037
|
+
},
|
|
10038
|
+
promptChoices: portal.profilePrompts,
|
|
10039
|
+
maxPrompts: portal.maxPrompts,
|
|
10040
|
+
photos: portal.photos
|
|
10041
|
+
};
|
|
10042
|
+
}
|
|
10043
|
+
var handleMemberProfile = async (req, url, env, ctx) => {
|
|
10044
|
+
if (url.pathname !== MEMBER_PROFILE_PATH && url.pathname !== MEMBER_PHOTO_PATH) return null;
|
|
10045
|
+
const portal = ctx.chapter.memberPortal;
|
|
10046
|
+
if (!portal) return json({ error: "not found" }, 404);
|
|
10047
|
+
const gate12 = await portalMemberGate(req, env, ctx);
|
|
10048
|
+
if (gate12 instanceof Response) return gate12;
|
|
10049
|
+
const { db, member } = gate12;
|
|
10050
|
+
if (url.pathname === MEMBER_PHOTO_PATH) return uploadPhoto(req, db, ctx, member);
|
|
10051
|
+
if (req.method === "GET") return json(await profileView(db, ctx, member));
|
|
10052
|
+
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
10053
|
+
const body = await readJsonObject(req, 16384);
|
|
10054
|
+
if (body instanceof Response) return body;
|
|
10055
|
+
const expectedRevision = body.expectedRevision;
|
|
10056
|
+
if (!Number.isInteger(expectedRevision) || Number(expectedRevision) < 0) {
|
|
10057
|
+
return json({ error: "expectedRevision must be a non-negative integer" }, 400);
|
|
10058
|
+
}
|
|
10059
|
+
const checked = validateProfilePatch(body, portal);
|
|
10060
|
+
if (!checked.ok) return json({ error: "invalid profile", errors: checked.errors }, 400);
|
|
10061
|
+
try {
|
|
10062
|
+
const saved = await saveMemberProfile(db, {
|
|
10063
|
+
mode: ctx.chapter.mode,
|
|
10064
|
+
chapterId: ctx.chapter.id,
|
|
10065
|
+
memberId: member.memberId,
|
|
10066
|
+
patch: checked.patch,
|
|
10067
|
+
expectedRevision: Number(expectedRevision)
|
|
10068
|
+
});
|
|
10069
|
+
return json({ ...await profileView(db, ctx, member), introducedBy: saved.review });
|
|
10070
|
+
} catch (error) {
|
|
10071
|
+
if (error instanceof Error && error.message === "member_profile_revision_conflict") {
|
|
10072
|
+
return json({ error: "your profile changed elsewhere; reload before saving" }, 409);
|
|
10073
|
+
}
|
|
10074
|
+
return json({ error: "profile could not be saved" }, 503);
|
|
10075
|
+
}
|
|
10076
|
+
};
|
|
10077
|
+
async function uploadPhoto(req, db, ctx, member) {
|
|
10078
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
10079
|
+
if (!ctx.chapter.memberPortal?.photos || !db.storage) return json({ error: "photos are not enabled" }, 404);
|
|
10080
|
+
const declared = Number(req.headers.get("content-length") ?? 0);
|
|
10081
|
+
if (declared > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
|
|
10082
|
+
const bytes = new Uint8Array(await req.arrayBuffer());
|
|
10083
|
+
if (bytes.length > MAX_PHOTO_BYTES) return json({ error: "photo is larger than 10 MB" }, 413);
|
|
10084
|
+
const image = cleanImage(bytes);
|
|
10085
|
+
if (!image) return json({ error: "photo must be a JPEG or PNG" }, 415);
|
|
10086
|
+
if (image.width > PHOTO_SIZE || image.height > PHOTO_SIZE) {
|
|
10087
|
+
return json({ error: `photo must be at most ${PHOTO_SIZE} by ${PHOTO_SIZE} pixels` }, 422);
|
|
10088
|
+
}
|
|
10089
|
+
const now = Date.now();
|
|
10090
|
+
const path = `member-photos/${encodeURIComponent(member.memberId)}/${now}.${image.extension}`;
|
|
10091
|
+
await db.storage.upload(path, new Blob([image.bytes], { type: image.contentType }), image.contentType, { private: true });
|
|
10092
|
+
const previous = await loadMemberProfile(db, member.memberId);
|
|
10093
|
+
await db.transact([{ t: "update", ns: "memberProfiles", id: member.memberId, attrs: {
|
|
10094
|
+
id: member.memberId,
|
|
10095
|
+
memberId: member.memberId,
|
|
10096
|
+
photoPath: path,
|
|
10097
|
+
photoUpdatedAt: now,
|
|
10098
|
+
updatedAt: now,
|
|
10099
|
+
...previous ? {} : { revision: 0 }
|
|
10100
|
+
} }], { mutationId: `member-photo:${member.memberId}:${now}` });
|
|
10101
|
+
const old = typeof previous?.photoPath === "string" ? previous.photoPath : null;
|
|
10102
|
+
if (old && old !== path) await db.storage.delete(old).catch(() => void 0);
|
|
10103
|
+
return json({ photoUrl: await db.storage.sign(path, 3600), width: image.width, height: image.height });
|
|
10104
|
+
}
|
|
10105
|
+
|
|
10106
|
+
// src/worker-routes-admin-member-tier.ts
|
|
10107
|
+
var PATH5 = /^\/api\/admin\/applications\/([^/]+)\/entitlement-tier$/;
|
|
10108
|
+
var handleAdminMemberTier = async (req, url, env, ctx) => {
|
|
10109
|
+
const match = PATH5.exec(url.pathname);
|
|
10110
|
+
if (!match) return null;
|
|
10111
|
+
if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
|
|
10112
|
+
if (!["GET", "POST", "DELETE"].includes(req.method)) return json({ error: "method not allowed" }, 405);
|
|
10113
|
+
const user = await ctx.verifyUser(req, env);
|
|
10114
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10115
|
+
const db = ctx.makeDb(env);
|
|
10116
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10117
|
+
const id7 = decodeURIComponent(match[1]);
|
|
10118
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
10119
|
+
if (!app || app.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
10120
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
10121
|
+
const tiers = group ? await loadTiers(db, group) : [];
|
|
10122
|
+
if (req.method === "GET") {
|
|
10123
|
+
const history = (await db.query({ memberTierChanges: { $: { where: { applicationId: id7 }, limit: 50 } } })).memberTierChanges ?? [];
|
|
10124
|
+
return json({
|
|
10125
|
+
purchasedTierId: app.tierId ?? null,
|
|
10126
|
+
entitledTierId: entitledTierId(app),
|
|
10127
|
+
entitlementTierEndsAt: app.entitlementTierEndsAt ?? null,
|
|
10128
|
+
tiers: tiers.filter((tier) => tier.active !== false).map(({ id: tierId, name: name2 }) => ({ id: tierId, name: name2 })),
|
|
10129
|
+
history: history.sort((a, b) => Number(b.at) - Number(a.at))
|
|
10130
|
+
});
|
|
10131
|
+
}
|
|
10132
|
+
const body = await readJsonObject(req, 8192);
|
|
10133
|
+
if (body instanceof Response) return body;
|
|
10134
|
+
if (app.status !== "approved" || app.canceled === true) return json({ error: "only an approved member can change tier" }, 409);
|
|
10135
|
+
const reason = typeof body.reason === "string" ? body.reason.trim() : "";
|
|
10136
|
+
if (!reason || reason.length > 500) return json({ error: "a reason of up to 500 characters is required" }, 400);
|
|
10137
|
+
const paymentReference = typeof body.paymentReference === "string" && body.paymentReference.trim() ? body.paymentReference.trim().slice(0, 200) : void 0;
|
|
10138
|
+
const now = Date.now();
|
|
10139
|
+
let toTierId;
|
|
10140
|
+
let endsAt;
|
|
10141
|
+
if (req.method === "POST") {
|
|
10142
|
+
toTierId = typeof body.tierId === "string" ? body.tierId : "";
|
|
10143
|
+
const tier = tiers.find((candidate) => candidate.id === toTierId);
|
|
10144
|
+
if (!tier || tier.active === false) return json({ error: "tierId must be an active tier" }, 400);
|
|
10145
|
+
if (body.endsAt !== void 0) {
|
|
10146
|
+
endsAt = Number(body.endsAt);
|
|
10147
|
+
if (!Number.isSafeInteger(endsAt) || endsAt <= now) return json({ error: "endsAt must be a future timestamp" }, 400);
|
|
10148
|
+
}
|
|
10149
|
+
} else if (app.entitlementTierId === void 0) return json({ error: "no entitlement tier to revert" }, 409);
|
|
10150
|
+
const fromTierId = entitledTierId(app, now) ?? void 0;
|
|
10151
|
+
const changeId = `tier-change:${id7}:${now}`;
|
|
10152
|
+
const ops = [
|
|
10153
|
+
{ t: "update", ns: "memberTierChanges", id: changeId, attrs: {
|
|
10154
|
+
id: changeId,
|
|
10155
|
+
applicationId: id7,
|
|
10156
|
+
reason,
|
|
10157
|
+
actor: user.email ?? user.userId,
|
|
10158
|
+
at: now,
|
|
10159
|
+
...fromTierId ? { fromTierId } : {},
|
|
10160
|
+
...toTierId ? { toTierId } : {},
|
|
10161
|
+
...endsAt ? { endsAt } : {},
|
|
10162
|
+
...paymentReference ? { paymentReference } : {}
|
|
10163
|
+
} },
|
|
10164
|
+
toTierId ? { t: "update", ns: "applications", id: id7, attrs: { entitlementTierId: toTierId, ...endsAt ? { entitlementTierEndsAt: endsAt } : {} } } : { t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierId", "entitlementTierEndsAt"] }
|
|
10165
|
+
];
|
|
10166
|
+
if (toTierId && !endsAt && app.entitlementTierEndsAt !== void 0) ops.push({ t: "retract", ns: "applications", id: id7, attrs: ["entitlementTierEndsAt"] });
|
|
10167
|
+
const current = app.entitlementTierId;
|
|
10168
|
+
try {
|
|
10169
|
+
await db.transact(ops, { mutationId: changeId, guards: [current === void 0 ? { ns: "applications", id: id7, exists: true, equals: { status: "approved" }, absent: ["entitlementTierId"] } : { ns: "applications", id: id7, exists: true, equals: { status: "approved", entitlementTierId: current } }] });
|
|
10170
|
+
} catch {
|
|
10171
|
+
return json({ error: "this member changed; reload and try again" }, 409);
|
|
10172
|
+
}
|
|
10173
|
+
return json({ ok: true, applicationId: id7, fromTierId: fromTierId ?? null, toTierId: toTierId ?? (typeof app.tierId === "string" ? app.tierId : null), endsAt: endsAt ?? null });
|
|
10174
|
+
};
|
|
10175
|
+
|
|
10176
|
+
// src/worker-routes-admin-portal-review.ts
|
|
10177
|
+
var PORTAL_REVIEW_PATH = "/api/admin/portal-review";
|
|
10178
|
+
var handleAdminPortalReview = async (req, url, env, ctx) => {
|
|
10179
|
+
if (url.pathname !== PORTAL_REVIEW_PATH && !url.pathname.startsWith(`${PORTAL_REVIEW_PATH}/`)) return null;
|
|
10180
|
+
if (!ctx.chapter.memberPortal) return json({ error: "not found" }, 404);
|
|
10181
|
+
const user = await ctx.verifyUser(req, env);
|
|
10182
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10183
|
+
const db = ctx.makeDb(env);
|
|
10184
|
+
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10185
|
+
if (req.method === "GET" && url.pathname === PORTAL_REVIEW_PATH) {
|
|
10186
|
+
const status2 = url.searchParams.get("status") ?? "open";
|
|
10187
|
+
const where = status2 === "all" ? {} : { status: status2 };
|
|
10188
|
+
const items = (await db.query({ portalReviewItems: { $: { where, limit: 1e3 } } })).portalReviewItems ?? [];
|
|
10189
|
+
return json({ items: items.sort((a, b) => Number(b.createdAt) - Number(a.createdAt)) });
|
|
10190
|
+
}
|
|
10191
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
10192
|
+
const id7 = decodeURIComponent(url.pathname.slice(PORTAL_REVIEW_PATH.length + 1));
|
|
10193
|
+
const body = await readJsonObject(req, 4096);
|
|
10194
|
+
if (body instanceof Response) return body;
|
|
10195
|
+
const decision = typeof body.decision === "string" ? body.decision.trim() : "";
|
|
10196
|
+
if (!decision || decision.length > 1e3) return json({ error: "a decision of up to 1,000 characters is required" }, 400);
|
|
10197
|
+
const item = (await db.query({ portalReviewItems: { $: { where: { id: id7 }, limit: 1 } } })).portalReviewItems?.[0];
|
|
10198
|
+
if (!item) return json({ error: "not found" }, 404);
|
|
10199
|
+
if (item.status !== "open") return json({ error: "already resolved" }, 409);
|
|
10200
|
+
const attrs = { status: "resolved", decision, resolvedBy: user.email ?? user.userId, resolvedAt: Date.now() };
|
|
10201
|
+
try {
|
|
10202
|
+
await db.transact([{ t: "update", ns: "portalReviewItems", id: id7, attrs }], {
|
|
10203
|
+
mutationId: `portal-review:${id7}:resolve`,
|
|
10204
|
+
guards: [{ ns: "portalReviewItems", id: id7, exists: true, equals: { status: "open" } }]
|
|
10205
|
+
});
|
|
10206
|
+
} catch {
|
|
10207
|
+
return json({ error: "already resolved" }, 409);
|
|
10208
|
+
}
|
|
10209
|
+
return json({ item: { ...item, ...attrs } });
|
|
10210
|
+
};
|
|
10211
|
+
|
|
10212
|
+
// src/worker-routes-member-portal.ts
|
|
10213
|
+
var PORTAL_ROUTES = [
|
|
10214
|
+
handleMemberProfile,
|
|
10215
|
+
handleAdminHubMembers,
|
|
10216
|
+
handleAdminMemberTier,
|
|
10217
|
+
handleAdminPortalReview
|
|
10218
|
+
];
|
|
10219
|
+
var handleMemberPortal = async (req, url, env, ctx) => {
|
|
10220
|
+
for (const route of PORTAL_ROUTES) {
|
|
10221
|
+
const response = await route(req, url, env, ctx);
|
|
10222
|
+
if (response) return response;
|
|
10223
|
+
}
|
|
10224
|
+
return null;
|
|
10225
|
+
};
|
|
10226
|
+
|
|
9073
10227
|
// src/worker-routes-admission-grants.ts
|
|
9074
10228
|
var import_db5 = require("@odla-ai/db");
|
|
9075
10229
|
var reject5 = (reason, status2, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status2);
|
|
@@ -9233,7 +10387,7 @@ async function networkWriteAllowed(req, db, user, ctx, sensitiveRead = false) {
|
|
|
9233
10387
|
|
|
9234
10388
|
// src/network-person-reference.ts
|
|
9235
10389
|
var import_db6 = require("@odla-ai/db");
|
|
9236
|
-
var
|
|
10390
|
+
var import_crm12 = require("@odla-ai/crm");
|
|
9237
10391
|
|
|
9238
10392
|
// src/worker-network-fetch.ts
|
|
9239
10393
|
var NetworkTargetTransportError = class extends Error {
|
|
@@ -9269,7 +10423,7 @@ function fetchNetworkTarget(env, target, destination, init) {
|
|
|
9269
10423
|
}
|
|
9270
10424
|
|
|
9271
10425
|
// src/worker-network-admission-state.ts
|
|
9272
|
-
var
|
|
10426
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
9273
10427
|
var MAX_ASSIGNMENTS = 2e3;
|
|
9274
10428
|
var admissionRows = (value) => Array.isArray(value) ? value : [];
|
|
9275
10429
|
function assignmentId(targetId, personId) {
|
|
@@ -9296,7 +10450,7 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
9296
10450
|
return isValidEmail(normalized) ? normalized : null;
|
|
9297
10451
|
}
|
|
9298
10452
|
async function grantInput(db, ctx, assignment, resolveEmail) {
|
|
9299
|
-
const person = resolveEmail ? null : await (0,
|
|
10453
|
+
const person = resolveEmail ? null : await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
|
|
9300
10454
|
const email = resolveEmail ? await resolveEmail(assignment.personId) : person ? personAdmissionEmail(ctx, person) : null;
|
|
9301
10455
|
if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
|
|
9302
10456
|
return {
|
|
@@ -9450,9 +10604,9 @@ function integer3(value, fallback, min, max) {
|
|
|
9450
10604
|
const parsed = Number(value);
|
|
9451
10605
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
9452
10606
|
}
|
|
9453
|
-
function projectRecord(record4,
|
|
10607
|
+
function projectRecord(record4, allowed, editable, stagesEnabled, crm) {
|
|
9454
10608
|
const fields = Object.fromEntries(
|
|
9455
|
-
|
|
10609
|
+
allowed.flatMap((name2) => Object.prototype.hasOwnProperty.call(record4.fields, name2) ? [[name2, record4.fields[name2]]] : [])
|
|
9456
10610
|
);
|
|
9457
10611
|
const def = crm.type(record4.type);
|
|
9458
10612
|
const editFields = editable.map((id7) => {
|
|
@@ -9502,7 +10656,7 @@ function chapterPersonReference(targetId, recordId) {
|
|
|
9502
10656
|
}
|
|
9503
10657
|
async function resolveAdmissionPersonEmail(db, env, ctx, target, personId) {
|
|
9504
10658
|
if (!personId.startsWith("chapter-person:")) {
|
|
9505
|
-
const person2 = await (0,
|
|
10659
|
+
const person2 = await (0, import_crm12.getRecord)({ crm: ctx.chapter.crm, db }, personId);
|
|
9506
10660
|
return person2 ? personAdmissionEmail(ctx, person2) : null;
|
|
9507
10661
|
}
|
|
9508
10662
|
const prefix = `chapter-person:${target.id}:`;
|
|
@@ -9562,7 +10716,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
9562
10716
|
}
|
|
9563
10717
|
|
|
9564
10718
|
// src/worker-routes-network-admission.ts
|
|
9565
|
-
var
|
|
10719
|
+
var PATH6 = "/api/admin/network/admissions";
|
|
9566
10720
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
9567
10721
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
9568
10722
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -9642,11 +10796,11 @@ async function parseBody(req) {
|
|
|
9642
10796
|
}
|
|
9643
10797
|
}
|
|
9644
10798
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
9645
|
-
if (![
|
|
10799
|
+
if (![PATH6, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
9646
10800
|
const auth = await gate2(req, env, ctx);
|
|
9647
10801
|
if ("response" in auth) return auth.response;
|
|
9648
10802
|
const db = auth.db;
|
|
9649
|
-
if (url.pathname ===
|
|
10803
|
+
if (url.pathname === PATH6 && req.method === "GET") {
|
|
9650
10804
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
9651
10805
|
const target2 = targetFor(ctx, targetId2);
|
|
9652
10806
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -9794,8 +10948,8 @@ function makeCalendar(env) {
|
|
|
9794
10948
|
}
|
|
9795
10949
|
async function firstRow3(db, ns, q) {
|
|
9796
10950
|
const res = await db.query({ [ns]: { $: q } });
|
|
9797
|
-
const
|
|
9798
|
-
return Array.isArray(
|
|
10951
|
+
const rows6 = res[ns];
|
|
10952
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
9799
10953
|
}
|
|
9800
10954
|
async function computeSlots(cal, cfg) {
|
|
9801
10955
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -10654,18 +11808,18 @@ async function startSubscription(req, env, ctx) {
|
|
|
10654
11808
|
|
|
10655
11809
|
// src/worker-payment-receipt.ts
|
|
10656
11810
|
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10657
|
-
var escape = (
|
|
11811
|
+
var escape = (text11) => text11.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10658
11812
|
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10659
11813
|
const id7 = url.searchParams.get("application");
|
|
10660
11814
|
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
10661
11815
|
if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
|
|
10662
11816
|
const db = ctx.makeDb(env);
|
|
10663
|
-
const
|
|
10664
|
-
const app =
|
|
11817
|
+
const rows6 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11818
|
+
const app = rows6.applications?.[0], intent = rows6.subscriptionCheckoutIntents?.[0];
|
|
10665
11819
|
if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
|
|
10666
11820
|
const receipt = intent.paymentReceipt;
|
|
10667
11821
|
if (!receipt) return json2({ pending: true }, 202);
|
|
10668
|
-
const
|
|
11822
|
+
const text11 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10669
11823
|
if (req.method === "POST") {
|
|
10670
11824
|
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10671
11825
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
@@ -10683,7 +11837,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10683
11837
|
template: "paymentReceipt",
|
|
10684
11838
|
to: String(intent.purchaserEmailNormalized),
|
|
10685
11839
|
applicationId: id7,
|
|
10686
|
-
vars: { chapterName: ctx.chapter.name, receiptText:
|
|
11840
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text11 },
|
|
10687
11841
|
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10688
11842
|
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10689
11843
|
});
|
|
@@ -10693,7 +11847,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
|
|
|
10693
11847
|
const nonce = crypto.randomUUID();
|
|
10694
11848
|
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10695
11849
|
<style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
|
|
10696
|
-
<h1>Payment receipt</h1><pre>${escape(
|
|
11850
|
+
<h1>Payment receipt</h1><pre>${escape(text11)}</pre><button id="print">Print / save PDF</button>
|
|
10697
11851
|
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10698
11852
|
headers: {
|
|
10699
11853
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -10726,7 +11880,7 @@ var handlePayments = async (req, url, env, ctx) => {
|
|
|
10726
11880
|
|
|
10727
11881
|
// src/worker-routes-gifts.ts
|
|
10728
11882
|
var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
|
|
10729
|
-
var
|
|
11883
|
+
var rows4 = (value) => Array.isArray(value) ? value : [];
|
|
10730
11884
|
var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
|
|
10731
11885
|
var userFor = async (req, env, ctx) => {
|
|
10732
11886
|
const user = await ctx.verifyUser(req, env);
|
|
@@ -10764,7 +11918,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
10764
11918
|
const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
10765
11919
|
return json({
|
|
10766
11920
|
offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
|
|
10767
|
-
claims: buyerGiftClaims(
|
|
11921
|
+
claims: buyerGiftClaims(rows4(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
|
|
10768
11922
|
refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
|
|
10769
11923
|
merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
|
|
10770
11924
|
});
|
|
@@ -10787,7 +11941,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
10787
11941
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
10788
11942
|
if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
|
|
10789
11943
|
const emailNormalized = normalizeChapterEmail(user.email);
|
|
10790
|
-
const applications =
|
|
11944
|
+
const applications = rows4((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
|
|
10791
11945
|
const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
10792
11946
|
try {
|
|
10793
11947
|
return json(await startGiftIntent(db, {
|
|
@@ -10917,9 +12071,9 @@ var receiptFor2 = (seat, released) => ({
|
|
|
10917
12071
|
});
|
|
10918
12072
|
async function regiftNamedSeat(db, input, now) {
|
|
10919
12073
|
membershipClock(now);
|
|
10920
|
-
const
|
|
12074
|
+
const name2 = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
10921
12075
|
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
10922
|
-
if (
|
|
12076
|
+
if (name2.length < 2 || name2.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
|
|
10923
12077
|
const state = await loadMembershipAuthorityState(db, input);
|
|
10924
12078
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
10925
12079
|
requireMembershipScope(seat, input);
|
|
@@ -10928,7 +12082,7 @@ async function regiftNamedSeat(db, input, now) {
|
|
|
10928
12082
|
if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
|
|
10929
12083
|
if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
|
|
10930
12084
|
const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
|
|
10931
|
-
if (seat.recipientEmailNormalized === email && seat.recipientName ===
|
|
12085
|
+
if (seat.recipientEmailNormalized === email && seat.recipientName === name2 && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
|
|
10932
12086
|
return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
|
|
10933
12087
|
}
|
|
10934
12088
|
if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
|
|
@@ -10966,7 +12120,7 @@ async function regiftNamedSeat(db, input, now) {
|
|
|
10966
12120
|
});
|
|
10967
12121
|
const ops = [
|
|
10968
12122
|
membershipUpdate("networkSeat", String(seat.id), {
|
|
10969
|
-
recipientName:
|
|
12123
|
+
recipientName: name2,
|
|
10970
12124
|
recipientEmailNormalized: email,
|
|
10971
12125
|
recipientClaimId: claimId,
|
|
10972
12126
|
status: "pending_acceptance",
|
|
@@ -11163,16 +12317,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
11163
12317
|
};
|
|
11164
12318
|
|
|
11165
12319
|
// src/named-seat-accepted-notice.ts
|
|
11166
|
-
var
|
|
12320
|
+
var text8 = (value) => typeof value === "string" ? value : "";
|
|
11167
12321
|
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
11168
12322
|
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
11169
|
-
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !
|
|
12323
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text8(purchaser.email)) {
|
|
11170
12324
|
return { sent: false, reason: "purchaser-missing" };
|
|
11171
12325
|
}
|
|
11172
12326
|
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
11173
12327
|
if (!group) return { sent: false, reason: "group-missing" };
|
|
11174
12328
|
const config = emailGroupFrom(group);
|
|
11175
|
-
const purchaserName = [purchaser.firstName, purchaser.lastName].map(
|
|
12329
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text8).join(" ").replace(/\s+/g, " ").trim();
|
|
11176
12330
|
return sendTemplated({
|
|
11177
12331
|
db,
|
|
11178
12332
|
envName: env.ODLA_ENV,
|
|
@@ -11184,12 +12338,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
|
11184
12338
|
}, {
|
|
11185
12339
|
group: config,
|
|
11186
12340
|
template: "seatAccepted",
|
|
11187
|
-
to:
|
|
12341
|
+
to: text8(purchaser.email),
|
|
11188
12342
|
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
11189
12343
|
applicationId: input.primaryApplicationId,
|
|
11190
12344
|
tierId: input.skuId,
|
|
11191
12345
|
vars: {
|
|
11192
|
-
firstName:
|
|
12346
|
+
firstName: text8(purchaser.firstName),
|
|
11193
12347
|
purchaserName,
|
|
11194
12348
|
recipientName: input.recipientName,
|
|
11195
12349
|
tierName: await tierName(db, group, input.skuId),
|
|
@@ -11401,10 +12555,10 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
11401
12555
|
};
|
|
11402
12556
|
|
|
11403
12557
|
// src/worker-routes-people-profile.ts
|
|
11404
|
-
var
|
|
12558
|
+
var import_crm14 = require("@odla-ai/crm");
|
|
11405
12559
|
|
|
11406
12560
|
// src/people-profile.ts
|
|
11407
|
-
var
|
|
12561
|
+
var import_crm13 = require("@odla-ai/crm");
|
|
11408
12562
|
var sameValue = (a, b) => (a == null || a === "") && (b == null || b === "") || JSON.stringify(a) === JSON.stringify(b);
|
|
11409
12563
|
function profileReadOnlyFields(record4, ctx) {
|
|
11410
12564
|
return [
|
|
@@ -11419,17 +12573,17 @@ async function updateChapterPerson(opts) {
|
|
|
11419
12573
|
const { db, env, ctx, record: record4 } = opts;
|
|
11420
12574
|
const input = Object.fromEntries(Object.entries(opts.input).filter(([key3, value]) => !sameValue(value, record4.fields[key3])));
|
|
11421
12575
|
const forbidden = profileReadOnlyFields(record4, ctx).filter((key3) => key3 in input);
|
|
11422
|
-
if (forbidden.length) throw new
|
|
12576
|
+
if (forbidden.length) throw new import_crm13.CrmInputError(
|
|
11423
12577
|
"Use the verified identity or application workflow for protected fields",
|
|
11424
12578
|
Object.fromEntries(forbidden.map((key3) => [key3, "This field is managed by its authoritative workflow"]))
|
|
11425
12579
|
);
|
|
11426
12580
|
const appId = record4.fields.applicationId;
|
|
11427
12581
|
const app = typeof appId === "string" && appId ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : null;
|
|
11428
12582
|
if (appId && (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || String(app.email).trim().toLowerCase() !== String(record4.primaryEmail).trim().toLowerCase())) {
|
|
11429
|
-
throw new
|
|
12583
|
+
throw new import_crm13.CrmInputError("The application identity must be repaired before editing this profile");
|
|
11430
12584
|
}
|
|
11431
12585
|
const prepared = ctx.chapter.crm.prepare(record4.type, input, record4);
|
|
11432
|
-
if (!prepared.ok) throw new
|
|
12586
|
+
if (!prepared.ok) throw new import_crm13.CrmInputError("invalid input", prepared.errors);
|
|
11433
12587
|
const appAttrs = {};
|
|
11434
12588
|
if (app) {
|
|
11435
12589
|
const mirrored = /* @__PURE__ */ new Set(["firstName", "lastName", "phone", "linkedin", ...ctx.chapter.application.crmFields]);
|
|
@@ -11458,7 +12612,7 @@ async function updateChapterPerson(opts) {
|
|
|
11458
12612
|
}] });
|
|
11459
12613
|
}
|
|
11460
12614
|
};
|
|
11461
|
-
return (0,
|
|
12615
|
+
return (0, import_crm13.updateRecord)({ crm: ctx.chapter.crm, db: projectionDb }, {
|
|
11462
12616
|
id: record4.id,
|
|
11463
12617
|
input,
|
|
11464
12618
|
expectedUpdatedAt: opts.expectedRevision ?? record4.updatedAt,
|
|
@@ -11503,7 +12657,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
11503
12657
|
} catch {
|
|
11504
12658
|
return json({ error: "invalid record id" }, 422);
|
|
11505
12659
|
}
|
|
11506
|
-
const record4 = await (0,
|
|
12660
|
+
const record4 = await (0, import_crm14.getRecord)({ crm: ctx.chapter.crm, db }, id7);
|
|
11507
12661
|
if (!record4) return json({ error: "not found" }, 404);
|
|
11508
12662
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
11509
12663
|
if (match?.[2]) {
|
|
@@ -11555,7 +12709,7 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
11555
12709
|
});
|
|
11556
12710
|
return json(result);
|
|
11557
12711
|
} catch (error) {
|
|
11558
|
-
if (error instanceof
|
|
12712
|
+
if (error instanceof import_crm14.CrmInputError) return json({ error: error.message, fields: error.fields }, 422);
|
|
11559
12713
|
if (error.code === "transact_guard_failed") return json({ error: "profile changed; reload before saving" }, 409);
|
|
11560
12714
|
return json({ error: "profile update failed" }, 503);
|
|
11561
12715
|
}
|
|
@@ -11650,8 +12804,8 @@ async function upcomingEvents(env) {
|
|
|
11650
12804
|
const res = await cal.availability.upcoming();
|
|
11651
12805
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
11652
12806
|
}
|
|
11653
|
-
function toReconcile(
|
|
11654
|
-
return
|
|
12807
|
+
function toReconcile(rows6) {
|
|
12808
|
+
return rows6.map((m) => ({
|
|
11655
12809
|
id: String(m.id),
|
|
11656
12810
|
applicationId: String(m.applicationId),
|
|
11657
12811
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -11673,12 +12827,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
11673
12827
|
const query = await db.query({
|
|
11674
12828
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
11675
12829
|
});
|
|
11676
|
-
let
|
|
11677
|
-
if (Number.isFinite(from))
|
|
11678
|
-
if (Number.isFinite(to))
|
|
12830
|
+
let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
12831
|
+
if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
12832
|
+
if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
11679
12833
|
let decisions = [];
|
|
11680
12834
|
try {
|
|
11681
|
-
decisions = reconcileMeetings(toReconcile(
|
|
12835
|
+
decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
|
|
11682
12836
|
} catch {
|
|
11683
12837
|
}
|
|
11684
12838
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -11705,7 +12859,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
11705
12859
|
};
|
|
11706
12860
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
11707
12861
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
11708
|
-
const meetings =
|
|
12862
|
+
const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
11709
12863
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
11710
12864
|
};
|
|
11711
12865
|
|
|
@@ -11730,10 +12884,10 @@ async function repairAccountPeoplePage(deps, after) {
|
|
|
11730
12884
|
}
|
|
11731
12885
|
|
|
11732
12886
|
// src/crm-application-repair.ts
|
|
11733
|
-
var
|
|
12887
|
+
var import_crm15 = require("@odla-ai/crm");
|
|
11734
12888
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
11735
12889
|
const deps = { crm: chapter.crm, db };
|
|
11736
|
-
const record4 = await (0,
|
|
12890
|
+
const record4 = await (0, import_crm15.getRecord)(deps, recordId);
|
|
11737
12891
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
11738
12892
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
11739
12893
|
const fields = record4.fields;
|
|
@@ -11751,7 +12905,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
11751
12905
|
}
|
|
11752
12906
|
const app = [...applications.values()][0];
|
|
11753
12907
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
11754
|
-
await (0,
|
|
12908
|
+
await (0, import_crm15.updateRecord)(deps, {
|
|
11755
12909
|
id: recordId,
|
|
11756
12910
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
11757
12911
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -11844,15 +12998,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
11844
12998
|
for (const a of data.applications ?? []) {
|
|
11845
12999
|
const key3 = String(a.email ?? "").toLowerCase();
|
|
11846
13000
|
if (!key3) continue;
|
|
11847
|
-
const
|
|
13001
|
+
const name2 = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
|
|
11848
13002
|
const row2 = people.get(key3);
|
|
11849
13003
|
if (row2) {
|
|
11850
13004
|
if (!row2.application) row2.application = applicationSummary(a);
|
|
11851
|
-
if (!row2.name) row2.name =
|
|
13005
|
+
if (!row2.name) row2.name = name2;
|
|
11852
13006
|
} else {
|
|
11853
13007
|
people.set(key3, {
|
|
11854
13008
|
email: String(a.email),
|
|
11855
|
-
name,
|
|
13009
|
+
name: name2,
|
|
11856
13010
|
userId: null,
|
|
11857
13011
|
role: null,
|
|
11858
13012
|
profile: null,
|
|
@@ -11861,10 +13015,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
11861
13015
|
});
|
|
11862
13016
|
}
|
|
11863
13017
|
}
|
|
11864
|
-
const
|
|
13018
|
+
const rows6 = [...people.values()].sort(
|
|
11865
13019
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
11866
13020
|
);
|
|
11867
|
-
return json({ people:
|
|
13021
|
+
return json({ people: rows6 });
|
|
11868
13022
|
};
|
|
11869
13023
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
11870
13024
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
@@ -12065,7 +13219,7 @@ async function gate3(req, env, ctx) {
|
|
|
12065
13219
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
12066
13220
|
return rawDb;
|
|
12067
13221
|
}
|
|
12068
|
-
var
|
|
13222
|
+
var rows5 = (v) => Array.isArray(v) ? v : [];
|
|
12069
13223
|
var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
12070
13224
|
if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
|
|
12071
13225
|
const got = await gate3(req, env, ctx);
|
|
@@ -12087,8 +13241,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12087
13241
|
crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
|
|
12088
13242
|
groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
|
|
12089
13243
|
});
|
|
12090
|
-
const apps =
|
|
12091
|
-
const incomplete =
|
|
13244
|
+
const apps = rows5(data.applications).filter((app) => chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME));
|
|
13245
|
+
const incomplete = rows5(data.applications).length >= 1e3 || rows5(data.crm_record).length >= 1e3 || rows5(data.meetings).length >= 500;
|
|
12092
13246
|
const applications = {
|
|
12093
13247
|
total: apps.length,
|
|
12094
13248
|
last7: apps.filter((a) => a.createdAt >= d7).length,
|
|
@@ -12104,7 +13258,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12104
13258
|
for (const a of apps) {
|
|
12105
13259
|
if (String(a.status) in pipeline) pipeline[String(a.status)] = (pipeline[String(a.status)] ?? 0) + 1;
|
|
12106
13260
|
}
|
|
12107
|
-
for (const r of
|
|
13261
|
+
for (const r of rows5(data.crm_record)) {
|
|
12108
13262
|
if (!appById.has(r.fields?.applicationId)) continue;
|
|
12109
13263
|
const s = r.stage;
|
|
12110
13264
|
if (s in pipeline) {
|
|
@@ -12112,7 +13266,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12112
13266
|
if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
|
|
12113
13267
|
}
|
|
12114
13268
|
}
|
|
12115
|
-
const meetings =
|
|
13269
|
+
const meetings = rows5(data.meetings).filter((m) => appById.has(m.applicationId));
|
|
12116
13270
|
const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
|
|
12117
13271
|
const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
|
|
12118
13272
|
const agenda = upcoming.slice(0, 8).map((m) => {
|
|
@@ -12135,12 +13289,12 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
12135
13289
|
const sk = await getVaultSecret(db, "stripe_secret_key");
|
|
12136
13290
|
if (sk) {
|
|
12137
13291
|
const subsRes = await chapterBillingSubscriptions(sk, ctx.chapter.id, env).catch(() => null);
|
|
12138
|
-
if (subsRes?.complete &&
|
|
13292
|
+
if (subsRes?.complete && rows5(data.applications).length < 1e3) {
|
|
12139
13293
|
const subs = subsRes.objects.filter((s) => {
|
|
12140
13294
|
const a = appById.get(s.metadata?.applicationId);
|
|
12141
13295
|
return a?.stripeSubscriptionId === s.id && a?.stripeCustomerId === s.customer;
|
|
12142
13296
|
});
|
|
12143
|
-
const intents = new Map(
|
|
13297
|
+
const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
|
|
12144
13298
|
const amount = (s) => chapterBillingAmount(s, intents.get(s.metadata?.applicationId), env);
|
|
12145
13299
|
const subMs = (s) => (s.created ?? 0) * 1e3;
|
|
12146
13300
|
membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
|
|
@@ -12179,9 +13333,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
12179
13333
|
const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
|
|
12180
13334
|
if (!subsRes) return json({ error: "billing lookup failed upstream" }, 502);
|
|
12181
13335
|
const subById = new Map(subsRes.objects.map((s) => [s.id, s]));
|
|
12182
|
-
const intents = new Map(
|
|
13336
|
+
const intents = new Map(rows5(data.subscriptionCheckoutIntents).map((i) => [i.applicationId, i]));
|
|
12183
13337
|
const billingRows = [];
|
|
12184
|
-
for (const a of
|
|
13338
|
+
for (const a of rows5(data.applications)) {
|
|
12185
13339
|
if (!chapterBillingApplication(a, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
|
|
12186
13340
|
if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
|
|
12187
13341
|
const candidate = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
|
|
@@ -12217,9 +13371,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
12217
13371
|
return json({
|
|
12218
13372
|
billingReady: true,
|
|
12219
13373
|
testMode,
|
|
12220
|
-
truncated: !subsRes.complete ||
|
|
13374
|
+
truncated: !subsRes.complete || rows5(data.applications).length >= 200,
|
|
12221
13375
|
rows: billingRows,
|
|
12222
|
-
summary: subsRes.complete &&
|
|
13376
|
+
summary: subsRes.complete && rows5(data.applications).length < 200 ? summary : null
|
|
12223
13377
|
});
|
|
12224
13378
|
};
|
|
12225
13379
|
|
|
@@ -12572,10 +13726,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
12572
13726
|
};
|
|
12573
13727
|
|
|
12574
13728
|
// src/email-admin-config.ts
|
|
12575
|
-
var
|
|
13729
|
+
var import_crm17 = require("@odla-ai/crm");
|
|
12576
13730
|
|
|
12577
13731
|
// src/email-readiness.ts
|
|
12578
|
-
var
|
|
13732
|
+
var import_crm16 = require("@odla-ai/crm");
|
|
12579
13733
|
|
|
12580
13734
|
// src/email-spam-signals.ts
|
|
12581
13735
|
var TRIGGER_PHRASES = [
|
|
@@ -12611,7 +13765,8 @@ var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
|
12611
13765
|
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
12612
13766
|
function emailSpamSignals(copy) {
|
|
12613
13767
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12614
|
-
const
|
|
13768
|
+
const text11 = typeof copy.text === "string" ? copy.text : "";
|
|
13769
|
+
const html = typeof copy.html === "string" ? copy.html : "";
|
|
12615
13770
|
const out = [];
|
|
12616
13771
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
12617
13772
|
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, "")));
|
|
@@ -12620,11 +13775,12 @@ function emailSpamSignals(copy) {
|
|
|
12620
13775
|
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
12621
13776
|
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
12622
13777
|
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
12623
|
-
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(
|
|
13778
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text11));
|
|
12624
13779
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12625
|
-
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(
|
|
12626
|
-
if (/!{2,}/.test(
|
|
12627
|
-
const urls = [
|
|
13780
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text11))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
13781
|
+
if (/!{2,}/.test(text11)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
13782
|
+
const urls = [...`${text11}
|
|
13783
|
+
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
12628
13784
|
const hosts = urls.map((url) => {
|
|
12629
13785
|
try {
|
|
12630
13786
|
return new URL(url).hostname;
|
|
@@ -12636,7 +13792,7 @@ function emailSpamSignals(copy) {
|
|
|
12636
13792
|
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
12637
13793
|
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
12638
13794
|
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
12639
|
-
if (urls.length && words(
|
|
13795
|
+
if (urls.length && words(text11).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
12640
13796
|
return out;
|
|
12641
13797
|
}
|
|
12642
13798
|
function emailSenderWarnings(input) {
|
|
@@ -12661,19 +13817,23 @@ function validateEmailReadiness(input) {
|
|
|
12661
13817
|
}
|
|
12662
13818
|
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
12663
13819
|
for (const tier of input.tiers ?? []) {
|
|
12664
|
-
if (!(0,
|
|
13820
|
+
if (!(0, import_crm16.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
12665
13821
|
}
|
|
12666
|
-
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0,
|
|
13822
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm16.isEmailTierName)(tier.name, tier.id));
|
|
12667
13823
|
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
12668
13824
|
const events = Object.entries(input.events).map(([key3, event2]) => {
|
|
12669
13825
|
const template = input.templates[key3];
|
|
12670
|
-
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0,
|
|
13826
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm16.validateEmailCopy)(template, { allowedVariables: event2.variables });
|
|
12671
13827
|
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
13828
|
+
const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
|
|
13829
|
+
if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
|
|
12672
13830
|
if (template) {
|
|
12673
|
-
const
|
|
12674
|
-
${template.text ?? ""}
|
|
12675
|
-
|
|
12676
|
-
|
|
13831
|
+
const copy = `${template.subject ?? ""}
|
|
13832
|
+
${template.text ?? ""}
|
|
13833
|
+
${html}`;
|
|
13834
|
+
const sectionOnly = new Set((0, import_crm16.templateSectionNames)(copy));
|
|
13835
|
+
for (const name2 of (0, import_crm16.templateVars)(copy)) if (name2 in input.groupValues && !input.groupValues[name2]?.trim() && !sectionOnly.has(name2)) {
|
|
13836
|
+
problems.push({ code: "value-missing", variable: name2, message: `Add the ${name2} used by this email.` });
|
|
12677
13837
|
}
|
|
12678
13838
|
}
|
|
12679
13839
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
@@ -12694,13 +13854,14 @@ var str2 = (value) => typeof value === "string" ? value : "";
|
|
|
12694
13854
|
async function emailSettings(db, ctx, env) {
|
|
12695
13855
|
const hub = ctx.chapter.mode === "hub";
|
|
12696
13856
|
const crmDeps3 = { db, crm: ctx.chapter.crm };
|
|
12697
|
-
const group = hub ? await (0,
|
|
13857
|
+
const group = hub ? await (0, import_crm17.getConfig)(crmDeps3) : (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
12698
13858
|
const raw2 = group?.emailTemplates;
|
|
12699
13859
|
const stored = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
|
|
12700
13860
|
const events = chapterEmailEvents(ctx.chapter);
|
|
12701
13861
|
const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
|
|
12702
13862
|
subject: str2(stored[key3]?.subject),
|
|
12703
13863
|
text: str2(stored[key3]?.text),
|
|
13864
|
+
html: str2(stored[key3]?.html),
|
|
12704
13865
|
enabled: stored[key3]?.enabled
|
|
12705
13866
|
}]));
|
|
12706
13867
|
let tiers = [], tiersUnavailable = false;
|
|
@@ -12741,18 +13902,20 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12741
13902
|
if (!group && ctx.chapter.mode !== "hub") return json({ error: "Email settings have not been configured for this chapter." }, 409);
|
|
12742
13903
|
const templates = body.emailTemplates;
|
|
12743
13904
|
if (!templates || typeof templates !== "object" || Array.isArray(templates)) return json({ error: "emailTemplates object required" }, 400);
|
|
12744
|
-
const
|
|
13905
|
+
const clean2 = {};
|
|
12745
13906
|
for (const [key3, value] of Object.entries(templates)) {
|
|
12746
13907
|
if (!events[key3]) continue;
|
|
12747
13908
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12748
13909
|
const t = value;
|
|
12749
13910
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12750
|
-
const subject = str2(t.subject).trim(),
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
13911
|
+
const subject = str2(t.subject).trim(), text11 = str2(t.text), html = str2(t.html);
|
|
13912
|
+
if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
|
|
13913
|
+
const issues = (0, import_crm17.validateEmailCopy)({ subject, text: text11 }, { allowedVariables: events[key3].variables });
|
|
13914
|
+
const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
|
|
13915
|
+
if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
|
|
13916
|
+
return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
|
|
12754
13917
|
}
|
|
12755
|
-
|
|
13918
|
+
clean2[key3] = { subject, text: text11, ...html.trim() ? { html } : {}, enabled: t.enabled };
|
|
12756
13919
|
}
|
|
12757
13920
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12758
13921
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|
|
@@ -12765,20 +13928,20 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12765
13928
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
12766
13929
|
if (ctx.chapter.mode === "hub") {
|
|
12767
13930
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
12768
|
-
await (0,
|
|
13931
|
+
await (0, import_crm17.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean2 }, notificationEmail, replyTo, debugEmail });
|
|
12769
13932
|
} else {
|
|
12770
13933
|
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
12771
13934
|
await db.transact([
|
|
12772
|
-
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates:
|
|
13935
|
+
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean2 } },
|
|
12773
13936
|
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
12774
13937
|
]);
|
|
12775
13938
|
}
|
|
12776
|
-
const warnings = Object.fromEntries(Object.entries(
|
|
13939
|
+
const warnings = Object.fromEntries(Object.entries(clean2).map(([key3, template]) => [key3, emailSpamSignals(template)]).filter(([, found]) => found.length));
|
|
12777
13940
|
return json({ ok: true, warnings });
|
|
12778
13941
|
}
|
|
12779
13942
|
|
|
12780
13943
|
// src/worker-routes-admin-comms.ts
|
|
12781
|
-
var
|
|
13944
|
+
var import_crm18 = require("@odla-ai/crm");
|
|
12782
13945
|
async function gate5(req, env, ctx) {
|
|
12783
13946
|
const rawDb = ctx.makeDb(env);
|
|
12784
13947
|
const u = await ctx.verifyUser(req, env);
|
|
@@ -12811,8 +13974,8 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
|
12811
13974
|
const got = await gate5(req, env, ctx);
|
|
12812
13975
|
if (got instanceof Response) return got;
|
|
12813
13976
|
if (ctx.chapter.mode === "hub") {
|
|
12814
|
-
const
|
|
12815
|
-
return json({ sends:
|
|
13977
|
+
const rows6 = await (0, import_crm18.listEmailLog)({ db: got, crm: ctx.chapter.crm }, { limit: 50 });
|
|
13978
|
+
return json({ sends: rows6.map((row2) => ({ ...row2, template: row2.templateId })) });
|
|
12816
13979
|
}
|
|
12817
13980
|
const { emailLog } = await got.query({ emailLog: { $: { where: { groupId: ctx.chapter.id }, order: { sentAt: "desc" }, limit: 50 } } });
|
|
12818
13981
|
const visible = [];
|
|
@@ -12908,7 +14071,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
12908
14071
|
};
|
|
12909
14072
|
|
|
12910
14073
|
// src/worker-routes-network.ts
|
|
12911
|
-
var
|
|
14074
|
+
var import_crm20 = require("@odla-ai/crm");
|
|
12912
14075
|
var import_db9 = require("@odla-ai/db");
|
|
12913
14076
|
|
|
12914
14077
|
// src/network-edge.ts
|
|
@@ -12934,7 +14097,7 @@ function unpairedResponse(unpaired) {
|
|
|
12934
14097
|
}
|
|
12935
14098
|
|
|
12936
14099
|
// src/worker-network-push.ts
|
|
12937
|
-
var
|
|
14100
|
+
var import_crm19 = require("@odla-ai/crm");
|
|
12938
14101
|
var import_db8 = require("@odla-ai/db");
|
|
12939
14102
|
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
12940
14103
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12946,7 +14109,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12946
14109
|
};
|
|
12947
14110
|
if (!secret) {
|
|
12948
14111
|
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
12949
|
-
await (0,
|
|
14112
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12950
14113
|
recordId: record4.id,
|
|
12951
14114
|
targetId: target.id,
|
|
12952
14115
|
status: "failed",
|
|
@@ -12985,7 +14148,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
12985
14148
|
const responseBody = await res.json().catch(() => ({}));
|
|
12986
14149
|
if (!res.ok) {
|
|
12987
14150
|
const error = responseBody.error ?? "follower rejected the record";
|
|
12988
|
-
await (0,
|
|
14151
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
12989
14152
|
recordId: record4.id,
|
|
12990
14153
|
targetId: target.id,
|
|
12991
14154
|
status: "failed",
|
|
@@ -13000,7 +14163,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
13000
14163
|
error
|
|
13001
14164
|
};
|
|
13002
14165
|
}
|
|
13003
|
-
await (0,
|
|
14166
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
13004
14167
|
recordId: record4.id,
|
|
13005
14168
|
targetId: target.id,
|
|
13006
14169
|
status: "delivered",
|
|
@@ -13016,7 +14179,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
13016
14179
|
};
|
|
13017
14180
|
} catch (err) {
|
|
13018
14181
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
13019
|
-
await (0,
|
|
14182
|
+
await (0, import_crm19.recordDeliveryAttempt)(crmDeps3, {
|
|
13020
14183
|
recordId: record4.id,
|
|
13021
14184
|
targetId: target.id,
|
|
13022
14185
|
status: "failed",
|
|
@@ -13042,7 +14205,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
13042
14205
|
return json({
|
|
13043
14206
|
targets: ctx.chapter.network.targets.map(({
|
|
13044
14207
|
id: id7,
|
|
13045
|
-
name,
|
|
14208
|
+
name: name2,
|
|
13046
14209
|
url: targetUrl,
|
|
13047
14210
|
fields,
|
|
13048
14211
|
sharedNotes,
|
|
@@ -13051,7 +14214,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
13051
14214
|
commercialParity
|
|
13052
14215
|
}) => ({
|
|
13053
14216
|
id: id7,
|
|
13054
|
-
name,
|
|
14217
|
+
name: name2,
|
|
13055
14218
|
url: targetUrl,
|
|
13056
14219
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
13057
14220
|
sharedNotes,
|
|
@@ -13069,7 +14232,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
13069
14232
|
const verified = await (0, import_db9.verifyFederatedRequest)(req, { secret });
|
|
13070
14233
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
13071
14234
|
const types = await Promise.all(
|
|
13072
|
-
Object.keys(ctx.chapter.crm.config.types).map((type) => (0,
|
|
14235
|
+
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm20.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
13073
14236
|
);
|
|
13074
14237
|
const activeConfiguration = ctx.chapter.signupControl ? await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV) : null;
|
|
13075
14238
|
const snapshot = {
|
|
@@ -13216,7 +14379,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
13216
14379
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
13217
14380
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
13218
14381
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
13219
|
-
const record4 = await (0,
|
|
14382
|
+
const record4 = await (0, import_crm20.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
13220
14383
|
if (!record4) return json({ error: "record not found" }, 404);
|
|
13221
14384
|
const results = await Promise.all(
|
|
13222
14385
|
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
@@ -13225,11 +14388,11 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
13225
14388
|
};
|
|
13226
14389
|
|
|
13227
14390
|
// src/worker-routes-network-records-follower.ts
|
|
13228
|
-
var
|
|
14391
|
+
var import_crm24 = require("@odla-ai/crm");
|
|
13229
14392
|
var import_db10 = require("@odla-ai/db");
|
|
13230
14393
|
|
|
13231
14394
|
// src/network-application-action.ts
|
|
13232
|
-
var
|
|
14395
|
+
var import_crm21 = require("@odla-ai/crm");
|
|
13233
14396
|
var ACTION_META = "chapterNetworkMutation";
|
|
13234
14397
|
async function networkApplicationAction(opts) {
|
|
13235
14398
|
const { db, env, ctx, record: record4, input, sourceId, activityId, fingerprint } = opts;
|
|
@@ -13319,7 +14482,7 @@ async function networkApplicationAction(opts) {
|
|
|
13319
14482
|
const current = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
|
|
13320
14483
|
if (!current || current.status !== input.stage) return json({ error: "application reconciliation pending", code: "action_pending" }, 503);
|
|
13321
14484
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) });
|
|
13322
|
-
const projected = await (0,
|
|
14485
|
+
const projected = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
13323
14486
|
if (!projected || projected.stage !== input.stage) return json({ error: "CRM reconciliation pending", code: "action_pending" }, 503);
|
|
13324
14487
|
const audit = (await db.query({ crm_activity: { $: { where: { id: activityId }, limit: 1 } } })).crm_activity?.[0];
|
|
13325
14488
|
const priorMeta = audit?.meta;
|
|
@@ -13329,7 +14492,7 @@ async function networkApplicationAction(opts) {
|
|
|
13329
14492
|
} },
|
|
13330
14493
|
{ t: "update", ns: "crm_activity", id: lockId, attrs: { meta: { state: "complete", activityId } } }
|
|
13331
14494
|
], { guards: [{ ns: "crm_activity", id: lockId, exists: true, equals: { meta: { state: "pending", activityId } } }] });
|
|
13332
|
-
const updated = await (0,
|
|
14495
|
+
const updated = await (0, import_crm21.getRecord)({ crm: ctx.chapter.crm, db }, record4.id);
|
|
13333
14496
|
if (!updated) return json({ error: "record not found" }, 404);
|
|
13334
14497
|
return { record: updated, duplicate: Boolean(receipt) };
|
|
13335
14498
|
}
|
|
@@ -13365,7 +14528,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
13365
14528
|
}
|
|
13366
14529
|
|
|
13367
14530
|
// src/worker-routes-network-records-search.ts
|
|
13368
|
-
var
|
|
14531
|
+
var import_crm22 = require("@odla-ai/crm");
|
|
13369
14532
|
var SEARCH_BATCH_SIZE = 1e3;
|
|
13370
14533
|
var MAX_READER_SEARCH_POPULATION = 1e4;
|
|
13371
14534
|
var ReaderSearchPopulationError = class extends Error {
|
|
@@ -13385,8 +14548,8 @@ function terms(value) {
|
|
|
13385
14548
|
function matchesReaderIdentity(record4, search, includeEmail) {
|
|
13386
14549
|
const query = terms(search);
|
|
13387
14550
|
if (!query.length) return true;
|
|
13388
|
-
const
|
|
13389
|
-
const visible = [
|
|
14551
|
+
const name2 = folded(record4.name);
|
|
14552
|
+
const visible = [name2, ...name2.split(/\s+/u).filter(Boolean)];
|
|
13390
14553
|
if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
|
|
13391
14554
|
return query.every((term) => visible.some((candidate) => candidate.includes(term)));
|
|
13392
14555
|
}
|
|
@@ -13406,23 +14569,23 @@ async function listVisibleSearchPopulation(db, where, window, count) {
|
|
|
13406
14569
|
(_, index2) => index2 * SEARCH_BATCH_SIZE
|
|
13407
14570
|
);
|
|
13408
14571
|
const pages = await Promise.all(offsets.map((offset) => db.query({
|
|
13409
|
-
[
|
|
14572
|
+
[import_crm22.CRM_NS.record]: { $: {
|
|
13410
14573
|
where,
|
|
13411
14574
|
order: { [window.sort.field]: window.sort.dir },
|
|
13412
14575
|
limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
|
|
13413
14576
|
offset
|
|
13414
14577
|
} }
|
|
13415
14578
|
})));
|
|
13416
|
-
return pages.flatMap((page) => (page[
|
|
14579
|
+
return pages.flatMap((page) => (page[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord));
|
|
13417
14580
|
}
|
|
13418
|
-
async function listReaderRecords(db, type,
|
|
14581
|
+
async function listReaderRecords(db, type, allowed, emailField, window) {
|
|
13419
14582
|
const where = baseWhere(type, window.stage);
|
|
13420
14583
|
const crmDb = db;
|
|
13421
|
-
const aggregate = await crmDb.aggregate(
|
|
14584
|
+
const aggregate = await crmDb.aggregate(import_crm22.CRM_NS.record, { count: true }, { where });
|
|
13422
14585
|
const candidateCount = aggregate.count ?? 0;
|
|
13423
14586
|
if (window.search) {
|
|
13424
14587
|
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
13425
|
-
const includeEmail = Boolean(emailField &&
|
|
14588
|
+
const includeEmail = Boolean(emailField && allowed.includes(emailField));
|
|
13426
14589
|
const matches2 = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
|
|
13427
14590
|
return {
|
|
13428
14591
|
records: matches2.slice(window.offset, window.offset + window.limit),
|
|
@@ -13430,7 +14593,7 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
13430
14593
|
};
|
|
13431
14594
|
}
|
|
13432
14595
|
const result = await crmDb.query({
|
|
13433
|
-
[
|
|
14596
|
+
[import_crm22.CRM_NS.record]: { $: {
|
|
13434
14597
|
where,
|
|
13435
14598
|
order: { [window.sort.field]: window.sort.dir },
|
|
13436
14599
|
limit: window.limit,
|
|
@@ -13438,19 +14601,19 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
13438
14601
|
} }
|
|
13439
14602
|
});
|
|
13440
14603
|
return {
|
|
13441
|
-
records: (result[
|
|
14604
|
+
records: (result[import_crm22.CRM_NS.record] ?? []).map(import_crm22.rowToRecord),
|
|
13442
14605
|
total: candidateCount
|
|
13443
14606
|
};
|
|
13444
14607
|
}
|
|
13445
14608
|
|
|
13446
14609
|
// src/network-mutation-receipt.ts
|
|
13447
|
-
var
|
|
14610
|
+
var import_crm23 = require("@odla-ai/crm");
|
|
13448
14611
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
13449
14612
|
async function priorMutation(db, id7) {
|
|
13450
14613
|
const result = await db.query({
|
|
13451
|
-
[
|
|
14614
|
+
[import_crm23.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
13452
14615
|
});
|
|
13453
|
-
const row2 = (result[
|
|
14616
|
+
const row2 = (result[import_crm23.CRM_NS.activity] ?? [])[0];
|
|
13454
14617
|
const meta = row2?.meta;
|
|
13455
14618
|
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
13456
14619
|
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint, pending: receipt.state === "pending", failed: receipt.state === "failed" } : null;
|
|
@@ -13474,7 +14637,7 @@ function mutationError(error) {
|
|
|
13474
14637
|
if (error?.code === "transact_guard_failed") {
|
|
13475
14638
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
13476
14639
|
}
|
|
13477
|
-
if (error instanceof
|
|
14640
|
+
if (error instanceof import_crm24.CrmInputError) {
|
|
13478
14641
|
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
13479
14642
|
}
|
|
13480
14643
|
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
@@ -13515,7 +14678,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13515
14678
|
const editable = reader.editableFields[input.type];
|
|
13516
14679
|
if (hasFields) {
|
|
13517
14680
|
const names = Object.keys(fields);
|
|
13518
|
-
if (!editable || names.length === 0 || names.length > 32 || names.some((
|
|
14681
|
+
if (!editable || names.length === 0 || names.length > 32 || names.some((name2) => !editable.includes(name2))) {
|
|
13519
14682
|
return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
|
|
13520
14683
|
}
|
|
13521
14684
|
} else if (!reader.stageTransitions.includes(input.type)) {
|
|
@@ -13529,7 +14692,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13529
14692
|
}
|
|
13530
14693
|
if (prior?.failed) return json({ error: "this operation was not applied; reload before choosing a new action", code: "action_failed" }, 409);
|
|
13531
14694
|
if (prior && !prior.pending) {
|
|
13532
|
-
const replayed = await (0,
|
|
14695
|
+
const replayed = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
13533
14696
|
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
13534
14697
|
const result = {
|
|
13535
14698
|
version: 1,
|
|
@@ -13539,7 +14702,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13539
14702
|
};
|
|
13540
14703
|
return json(result);
|
|
13541
14704
|
}
|
|
13542
|
-
const record4 = await (0,
|
|
14705
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
13543
14706
|
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
13544
14707
|
if (!prior?.pending && record4.updatedAt !== input.expectedRevision) {
|
|
13545
14708
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
@@ -13580,7 +14743,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13580
14743
|
});
|
|
13581
14744
|
}
|
|
13582
14745
|
const capabilities = await operationalReaderRecord(record4, reader, db, env, ctx);
|
|
13583
|
-
if (hasFields && Object.keys(fields).some((
|
|
14746
|
+
if (hasFields && Object.keys(fields).some((name2) => !capabilities.edit?.fields.some((field) => field.id === name2))) {
|
|
13584
14747
|
return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
|
|
13585
14748
|
}
|
|
13586
14749
|
const deps = { crm: ctx.chapter.crm, db };
|
|
@@ -13602,7 +14765,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13602
14765
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
13603
14766
|
});
|
|
13604
14767
|
}
|
|
13605
|
-
const updated = hasFields ? await (0,
|
|
14768
|
+
const updated = hasFields ? await (0, import_crm24.updateRecord)(deps, {
|
|
13606
14769
|
id: record4.id,
|
|
13607
14770
|
input: fields,
|
|
13608
14771
|
mutationId: activityId,
|
|
@@ -13613,7 +14776,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13613
14776
|
authorId: `network:${verified.sender}`,
|
|
13614
14777
|
meta: auditMeta
|
|
13615
14778
|
}
|
|
13616
|
-
}) : await (0,
|
|
14779
|
+
}) : await (0, import_crm24.setStage)(deps, {
|
|
13617
14780
|
id: record4.id,
|
|
13618
14781
|
to: input.stage,
|
|
13619
14782
|
authorId: `network:${verified.sender}`,
|
|
@@ -13635,12 +14798,12 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13635
14798
|
}
|
|
13636
14799
|
}
|
|
13637
14800
|
const type = url.searchParams.get("type") ?? "";
|
|
13638
|
-
const
|
|
13639
|
-
if (!IDENTIFIER.test(type) || !
|
|
14801
|
+
const allowed = reader.fields[type];
|
|
14802
|
+
if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
|
|
13640
14803
|
const recordId = url.searchParams.get("recordId");
|
|
13641
14804
|
if (recordId !== null) {
|
|
13642
14805
|
if (!RECORD_ID.test(recordId)) return json({ error: "invalid record id" }, 400);
|
|
13643
|
-
const record4 = await (0,
|
|
14806
|
+
const record4 = await (0, import_crm24.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
13644
14807
|
if (!record4 || record4.type !== type || record4.archived) return json({ error: "record not found" }, 404);
|
|
13645
14808
|
return json({
|
|
13646
14809
|
version: 1,
|
|
@@ -13664,7 +14827,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
13664
14827
|
page = await listReaderRecords(
|
|
13665
14828
|
db,
|
|
13666
14829
|
type,
|
|
13667
|
-
|
|
14830
|
+
allowed,
|
|
13668
14831
|
typeDef.emailField,
|
|
13669
14832
|
window
|
|
13670
14833
|
);
|
|
@@ -13745,8 +14908,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
13745
14908
|
async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
13746
14909
|
const fields = input.fields;
|
|
13747
14910
|
if (fields) {
|
|
13748
|
-
const
|
|
13749
|
-
if (!
|
|
14911
|
+
const allowed = target.editableFields[input.type];
|
|
14912
|
+
if (!allowed || Object.keys(fields).some((name2) => !allowed.includes(name2))) {
|
|
13750
14913
|
return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
|
|
13751
14914
|
}
|
|
13752
14915
|
} else if (!target.stageTransitions.includes(input.type)) {
|
|
@@ -13935,7 +15098,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
13935
15098
|
};
|
|
13936
15099
|
|
|
13937
15100
|
// src/worker-routes-network-shared-notes.ts
|
|
13938
|
-
var
|
|
15101
|
+
var import_crm25 = require("@odla-ai/crm");
|
|
13939
15102
|
var import_db12 = require("@odla-ai/db");
|
|
13940
15103
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
13941
15104
|
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
@@ -13966,7 +15129,7 @@ function sharedNote(activity, sourceId) {
|
|
|
13966
15129
|
}
|
|
13967
15130
|
async function listSharedNotes(db, sourceId, recordId) {
|
|
13968
15131
|
const result = await db.query({
|
|
13969
|
-
[
|
|
15132
|
+
[import_crm25.CRM_NS.activity]: {
|
|
13970
15133
|
$: {
|
|
13971
15134
|
where: {
|
|
13972
15135
|
and: [
|
|
@@ -13979,7 +15142,7 @@ async function listSharedNotes(db, sourceId, recordId) {
|
|
|
13979
15142
|
}
|
|
13980
15143
|
}
|
|
13981
15144
|
});
|
|
13982
|
-
return (result[
|
|
15145
|
+
return (result[import_crm25.CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
13983
15146
|
const note = sharedNote(activity, sourceId);
|
|
13984
15147
|
return note ? [note] : [];
|
|
13985
15148
|
});
|
|
@@ -14007,7 +15170,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
14007
15170
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
14008
15171
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
14009
15172
|
}
|
|
14010
|
-
const record4 = await (0,
|
|
15173
|
+
const record4 = await (0, import_crm25.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
14011
15174
|
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
14012
15175
|
if (req.method === "GET") {
|
|
14013
15176
|
return json({
|
|
@@ -14029,7 +15192,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
14029
15192
|
}
|
|
14030
15193
|
const createdAt = Date.now();
|
|
14031
15194
|
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
14032
|
-
const result = await (0,
|
|
15195
|
+
const result = await (0, import_crm25.addActivity)(
|
|
14033
15196
|
{
|
|
14034
15197
|
crm: ctx.chapter.crm,
|
|
14035
15198
|
db,
|
|
@@ -14590,20 +15753,20 @@ async function networkPersonBulkSha256(value) {
|
|
|
14590
15753
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
14591
15754
|
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
14592
15755
|
}
|
|
14593
|
-
function assertNetworkPersonBulkRows(
|
|
14594
|
-
if (!Array.isArray(
|
|
15756
|
+
function assertNetworkPersonBulkRows(rows6) {
|
|
15757
|
+
if (!Array.isArray(rows6) || rows6.length < 1) {
|
|
14595
15758
|
throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
|
|
14596
15759
|
}
|
|
14597
|
-
if (
|
|
15760
|
+
if (rows6.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
|
|
14598
15761
|
throw new NetworkPersonRegistryError(
|
|
14599
15762
|
"bulk_rows_exceeded",
|
|
14600
15763
|
`bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
|
|
14601
15764
|
);
|
|
14602
15765
|
}
|
|
14603
|
-
return
|
|
15766
|
+
return rows6;
|
|
14604
15767
|
}
|
|
14605
|
-
async function networkPersonBulkBatchDigest(
|
|
14606
|
-
const bounded = assertNetworkPersonBulkRows(
|
|
15768
|
+
async function networkPersonBulkBatchDigest(rows6) {
|
|
15769
|
+
const bounded = assertNetworkPersonBulkRows(rows6);
|
|
14607
15770
|
return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
|
|
14608
15771
|
}
|
|
14609
15772
|
async function networkPersonRegistryStateDigest(state) {
|
|
@@ -14618,7 +15781,7 @@ async function networkPersonRegistryStateDigest(state) {
|
|
|
14618
15781
|
function rejectedValidation(errors) {
|
|
14619
15782
|
return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
|
|
14620
15783
|
}
|
|
14621
|
-
async function classifyNetworkPersonBulkRows(
|
|
15784
|
+
async function classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state) {
|
|
14622
15785
|
const aliasIds = new Set(state.aliases.map((alias) => alias.id));
|
|
14623
15786
|
const batchAliases = /* @__PURE__ */ new Set();
|
|
14624
15787
|
const claimOwners = /* @__PURE__ */ new Map();
|
|
@@ -14629,8 +15792,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
|
14629
15792
|
}
|
|
14630
15793
|
const results = [];
|
|
14631
15794
|
const values = [];
|
|
14632
|
-
for (let index2 = 0; index2 <
|
|
14633
|
-
const validation = validateNetworkPersonImport(
|
|
15795
|
+
for (let index2 = 0; index2 < rows6.length; index2 += 1) {
|
|
15796
|
+
const validation = validateNetworkPersonImport(rows6[index2], allowedChapterIds);
|
|
14634
15797
|
if (!validation.ok || !validation.value) {
|
|
14635
15798
|
const errors = validation.errors.slice(0, 8);
|
|
14636
15799
|
results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
|
|
@@ -14663,8 +15826,8 @@ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
|
14663
15826
|
}
|
|
14664
15827
|
return { rows: results, values };
|
|
14665
15828
|
}
|
|
14666
|
-
async function previewNetworkPersonBulkImport(db,
|
|
14667
|
-
const bounded = assertNetworkPersonBulkRows(
|
|
15829
|
+
async function previewNetworkPersonBulkImport(db, rows6, allowedChapterIds) {
|
|
15830
|
+
const bounded = assertNetworkPersonBulkRows(rows6);
|
|
14668
15831
|
const state = await loadNetworkPersonRegistry(db);
|
|
14669
15832
|
const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
|
|
14670
15833
|
const counts = networkPersonBulkEmptyCounts();
|
|
@@ -14695,8 +15858,8 @@ function eventCounts(event2) {
|
|
|
14695
15858
|
}
|
|
14696
15859
|
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
14697
15860
|
if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
|
|
14698
|
-
const
|
|
14699
|
-
const batchDigest = await networkPersonBulkBatchDigest(
|
|
15861
|
+
const rows6 = assertNetworkPersonBulkRows(input.rows);
|
|
15862
|
+
const batchDigest = await networkPersonBulkBatchDigest(rows6);
|
|
14700
15863
|
if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
|
|
14701
15864
|
throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
|
|
14702
15865
|
}
|
|
@@ -14715,7 +15878,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14715
15878
|
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
14716
15879
|
batchDigest,
|
|
14717
15880
|
stateDigest: eventString(completed, "stateDigest"),
|
|
14718
|
-
rowCount: Number(completed.detail.rowCount) ||
|
|
15881
|
+
rowCount: Number(completed.detail.rowCount) || rows6.length,
|
|
14719
15882
|
counts: eventCounts(completed),
|
|
14720
15883
|
rows: [],
|
|
14721
15884
|
replayed: true
|
|
@@ -14723,7 +15886,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14723
15886
|
}
|
|
14724
15887
|
let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
14725
15888
|
if (!started) {
|
|
14726
|
-
const preview = await previewNetworkPersonBulkImport(db,
|
|
15889
|
+
const preview = await previewNetworkPersonBulkImport(db, rows6, allowedChapterIds);
|
|
14727
15890
|
if (preview.stateDigest !== input.expectedStateDigest) {
|
|
14728
15891
|
throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
|
|
14729
15892
|
}
|
|
@@ -14732,7 +15895,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14732
15895
|
networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
|
|
14733
15896
|
batchDigest,
|
|
14734
15897
|
stateDigest: input.expectedStateDigest,
|
|
14735
|
-
rowCount:
|
|
15898
|
+
rowCount: rows6.length,
|
|
14736
15899
|
previewCounts: preview.counts
|
|
14737
15900
|
})
|
|
14738
15901
|
], {
|
|
@@ -14748,7 +15911,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14748
15911
|
if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
|
|
14749
15912
|
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
|
|
14750
15913
|
}
|
|
14751
|
-
const classified = await classifyNetworkPersonBulkRows(
|
|
15914
|
+
const classified = await classifyNetworkPersonBulkRows(rows6, allowedChapterIds, state);
|
|
14752
15915
|
const results = [];
|
|
14753
15916
|
const counts = networkPersonBulkEmptyCounts();
|
|
14754
15917
|
for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
|
|
@@ -14787,10 +15950,10 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14787
15950
|
}
|
|
14788
15951
|
if (counts.failed === 0) {
|
|
14789
15952
|
await db.transact([
|
|
14790
|
-
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now +
|
|
15953
|
+
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows6.length + 1, {
|
|
14791
15954
|
batchDigest,
|
|
14792
15955
|
stateDigest: input.expectedStateDigest,
|
|
14793
|
-
rowCount:
|
|
15956
|
+
rowCount: rows6.length,
|
|
14794
15957
|
counts
|
|
14795
15958
|
})
|
|
14796
15959
|
], { mutationId: completedMutationId });
|
|
@@ -14799,7 +15962,7 @@ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, a
|
|
|
14799
15962
|
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
14800
15963
|
batchDigest,
|
|
14801
15964
|
stateDigest: input.expectedStateDigest,
|
|
14802
|
-
rowCount:
|
|
15965
|
+
rowCount: rows6.length,
|
|
14803
15966
|
counts,
|
|
14804
15967
|
rows: results
|
|
14805
15968
|
};
|
|
@@ -14920,7 +16083,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
14920
16083
|
}
|
|
14921
16084
|
|
|
14922
16085
|
// src/worker-routes-network-person-registry.ts
|
|
14923
|
-
var
|
|
16086
|
+
var PATH7 = "/api/admin/network-person-registry";
|
|
14924
16087
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
14925
16088
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
14926
16089
|
var positiveRevision = (value) => {
|
|
@@ -14945,7 +16108,7 @@ function registryError(error) {
|
|
|
14945
16108
|
return null;
|
|
14946
16109
|
}
|
|
14947
16110
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
14948
|
-
if (url.pathname !==
|
|
16111
|
+
if (url.pathname !== PATH7) return null;
|
|
14949
16112
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
14950
16113
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
14951
16114
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -14957,9 +16120,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
14957
16120
|
if (req.method === "POST") {
|
|
14958
16121
|
let body;
|
|
14959
16122
|
try {
|
|
14960
|
-
const
|
|
14961
|
-
if (new TextEncoder().encode(
|
|
14962
|
-
const parsed = JSON.parse(
|
|
16123
|
+
const text11 = await req.text();
|
|
16124
|
+
if (new TextEncoder().encode(text11).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
16125
|
+
const parsed = JSON.parse(text11);
|
|
14963
16126
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
14964
16127
|
body = parsed;
|
|
14965
16128
|
} catch {
|
|
@@ -15128,7 +16291,7 @@ var import_db13 = require("@odla-ai/db");
|
|
|
15128
16291
|
// src/network-commercial-parity.ts
|
|
15129
16292
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
15130
16293
|
var MAX_RECORDS = 500;
|
|
15131
|
-
var
|
|
16294
|
+
var text9 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
15132
16295
|
function parityMessage(partition, kind, value) {
|
|
15133
16296
|
return [
|
|
15134
16297
|
"chapter-commercial-parity",
|
|
@@ -15160,18 +16323,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
15160
16323
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
15161
16324
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
15162
16325
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
15163
|
-
const eligible = input.rows.filter((row2) =>
|
|
16326
|
+
const eligible = input.rows.filter((row2) => text9(row2.id) && (text9(row2.stripeCustomerId) || text9(row2.stripeSubscriptionId)));
|
|
15164
16327
|
const selected2 = eligible.slice(0, maxRecords);
|
|
15165
16328
|
const key3 = await parityKey(input.secret);
|
|
15166
16329
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
15167
|
-
const applicationId =
|
|
15168
|
-
const customerId =
|
|
15169
|
-
const subscriptionId =
|
|
16330
|
+
const applicationId = text9(row2.id);
|
|
16331
|
+
const customerId = text9(row2.stripeCustomerId);
|
|
16332
|
+
const subscriptionId = text9(row2.stripeSubscriptionId);
|
|
15170
16333
|
return {
|
|
15171
16334
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
15172
16335
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
15173
16336
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
15174
|
-
status:
|
|
16337
|
+
status: text9(row2.status, 64),
|
|
15175
16338
|
canceled: row2.canceled === true
|
|
15176
16339
|
};
|
|
15177
16340
|
}));
|
|
@@ -15197,7 +16360,7 @@ function validSnapshot(snapshot, source) {
|
|
|
15197
16360
|
if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
|
|
15198
16361
|
const seen = /* @__PURE__ */ new Set();
|
|
15199
16362
|
for (const record4 of records) {
|
|
15200
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
16363
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text9(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
15201
16364
|
const key3 = recordKey(record4);
|
|
15202
16365
|
if (seen.has(key3)) return false;
|
|
15203
16366
|
seen.add(key3);
|
|
@@ -15208,16 +16371,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
15208
16371
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
15209
16372
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
15210
16373
|
const key3 = await parityKey(input.secret);
|
|
15211
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
15212
|
-
const customerId =
|
|
15213
|
-
const subscriptionId =
|
|
16374
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text9(row2.applicationId) && text9(row2.objectId)).map(async (row2) => {
|
|
16375
|
+
const customerId = text9(row2.customerId);
|
|
16376
|
+
const subscriptionId = text9(row2.subscriptionId) ?? text9(row2.objectId);
|
|
15214
16377
|
return {
|
|
15215
16378
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
15216
16379
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
15217
16380
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
15218
16381
|
status: null,
|
|
15219
16382
|
canceled: false,
|
|
15220
|
-
stripeStatus:
|
|
16383
|
+
stripeStatus: text9(row2.status, 64),
|
|
15221
16384
|
stripeCanceled: row2.status === "canceled"
|
|
15222
16385
|
};
|
|
15223
16386
|
}));
|
|
@@ -15299,14 +16462,14 @@ var PAGE2 = 500;
|
|
|
15299
16462
|
var MAX_SCAN = 5e3;
|
|
15300
16463
|
var MAX_RECORDS2 = 500;
|
|
15301
16464
|
async function localCommercialRows(db) {
|
|
15302
|
-
const
|
|
16465
|
+
const rows6 = [];
|
|
15303
16466
|
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
15304
16467
|
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
15305
16468
|
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
15306
|
-
|
|
15307
|
-
if (page.length < PAGE2) return { rows:
|
|
16469
|
+
rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
|
|
16470
|
+
if (page.length < PAGE2) return { rows: rows6, truncated: false };
|
|
15308
16471
|
}
|
|
15309
|
-
return { rows:
|
|
16472
|
+
return { rows: rows6, truncated: true };
|
|
15310
16473
|
}
|
|
15311
16474
|
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
15312
16475
|
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
@@ -15441,8 +16604,8 @@ async function applyHostedMembershipDispute(env, ctx, fact, now) {
|
|
|
15441
16604
|
const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
|
|
15442
16605
|
return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
|
|
15443
16606
|
}
|
|
15444
|
-
const
|
|
15445
|
-
const member =
|
|
16607
|
+
const rows6 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
|
|
16608
|
+
const member = rows6.find((row2) => disputedChargeCoversMembership(charge, row2));
|
|
15446
16609
|
if (!member) return { disposition: "not_applicable" };
|
|
15447
16610
|
const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
|
|
15448
16611
|
return { disposition: "applied", membershipId: receipt.membershipId };
|
|
@@ -15658,7 +16821,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
15658
16821
|
};
|
|
15659
16822
|
|
|
15660
16823
|
// src/worker-routes-formation.ts
|
|
15661
|
-
var
|
|
16824
|
+
var import_crm26 = require("@odla-ai/crm");
|
|
15662
16825
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
15663
16826
|
var handleFormation = async (req, url, env, ctx) => {
|
|
15664
16827
|
const formation = ctx.chapter.formation;
|
|
@@ -15686,15 +16849,15 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
15686
16849
|
if (submissionId && !SUBMISSION.test(submissionId)) {
|
|
15687
16850
|
return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
|
|
15688
16851
|
}
|
|
15689
|
-
const
|
|
15690
|
-
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !
|
|
16852
|
+
const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
|
|
16853
|
+
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
|
|
15691
16854
|
if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
|
|
15692
16855
|
for (const field of formation.required) {
|
|
15693
16856
|
const value = body[field];
|
|
15694
16857
|
if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
|
|
15695
16858
|
}
|
|
15696
16859
|
const input = {};
|
|
15697
|
-
for (const field of
|
|
16860
|
+
for (const field of allowed) {
|
|
15698
16861
|
const value = body[field];
|
|
15699
16862
|
if (value === void 0) continue;
|
|
15700
16863
|
const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
|
|
@@ -15710,7 +16873,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
15710
16873
|
const db = ctx.makeDb(env);
|
|
15711
16874
|
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
15712
16875
|
try {
|
|
15713
|
-
const created = await (0,
|
|
16876
|
+
const created = await (0, import_crm26.createRecord)(
|
|
15714
16877
|
{
|
|
15715
16878
|
crm: ctx.chapter.crm,
|
|
15716
16879
|
db,
|
|
@@ -15758,10 +16921,10 @@ var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
|
|
|
15758
16921
|
var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
|
|
15759
16922
|
var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
|
|
15760
16923
|
var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15761
|
-
function unknownKeys3(value,
|
|
15762
|
-
for (const key3 of Object.keys(value)) if (!
|
|
16924
|
+
function unknownKeys3(value, allowed, path, errors) {
|
|
16925
|
+
for (const key3 of Object.keys(value)) if (!allowed.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
15763
16926
|
}
|
|
15764
|
-
function
|
|
16927
|
+
function text10(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
15765
16928
|
if (value === void 0 && !required) return void 0;
|
|
15766
16929
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
15767
16930
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -15787,7 +16950,7 @@ function scopeValue(value, path, errors) {
|
|
|
15787
16950
|
return { kind: raw2.kind };
|
|
15788
16951
|
}
|
|
15789
16952
|
if (raw2.kind === "chapter") {
|
|
15790
|
-
const chapterId =
|
|
16953
|
+
const chapterId = text10(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
15791
16954
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
15792
16955
|
return { kind: "chapter", chapterId };
|
|
15793
16956
|
}
|
|
@@ -15801,22 +16964,22 @@ function resourceValue(value, path, errors) {
|
|
|
15801
16964
|
return null;
|
|
15802
16965
|
}
|
|
15803
16966
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
15804
|
-
const id7 =
|
|
16967
|
+
const id7 = text10(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
15805
16968
|
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
15806
|
-
const kind =
|
|
16969
|
+
const kind = text10(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
15807
16970
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
15808
|
-
const title =
|
|
15809
|
-
const summary =
|
|
15810
|
-
const status2 =
|
|
16971
|
+
const title = text10(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
16972
|
+
const summary = text10(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
16973
|
+
const status2 = text10(raw2.status, `${path}.status`, errors) ?? "";
|
|
15811
16974
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
15812
16975
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
15813
16976
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
15814
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
16977
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text10(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
15815
16978
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
15816
16979
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
15817
16980
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
15818
|
-
const bodyMarkdown =
|
|
15819
|
-
const url =
|
|
16981
|
+
const bodyMarkdown = text10(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
16982
|
+
const url = text10(raw2.url, `${path}.url`, errors, 2048, false);
|
|
15820
16983
|
if (url) {
|
|
15821
16984
|
try {
|
|
15822
16985
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -15828,9 +16991,9 @@ function resourceValue(value, path, errors) {
|
|
|
15828
16991
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
15829
16992
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
15830
16993
|
const brandAsset = asset ? {
|
|
15831
|
-
bookId:
|
|
15832
|
-
assetId:
|
|
15833
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
16994
|
+
bookId: text10(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
16995
|
+
assetId: text10(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
16996
|
+
...asset.filename === void 0 ? {} : { filename: text10(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
15834
16997
|
} : void 0;
|
|
15835
16998
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
15836
16999
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -15845,17 +17008,17 @@ function resourceValue(value, path, errors) {
|
|
|
15845
17008
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
15846
17009
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
15847
17010
|
const promotedFrom = promotedRaw ? {
|
|
15848
|
-
chapterId:
|
|
15849
|
-
resourceId:
|
|
17011
|
+
chapterId: text10(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
17012
|
+
resourceId: text10(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
15850
17013
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
15851
17014
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
15852
|
-
promotedBy:
|
|
17015
|
+
promotedBy: text10(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
15853
17016
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
15854
17017
|
} : void 0;
|
|
15855
17018
|
const provenance2 = {
|
|
15856
|
-
createdBy:
|
|
17019
|
+
createdBy: text10(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
15857
17020
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
15858
|
-
updatedBy:
|
|
17021
|
+
updatedBy: text10(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
15859
17022
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
15860
17023
|
...promotedFrom ? { promotedFrom } : {}
|
|
15861
17024
|
};
|
|
@@ -15874,7 +17037,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15874
17037
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
15875
17038
|
const sourceType = sourceRaw?.type;
|
|
15876
17039
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
15877
|
-
const sourceAppId =
|
|
17040
|
+
const sourceAppId = text10(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
15878
17041
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
15879
17042
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
15880
17043
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -15882,8 +17045,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
15882
17045
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
15883
17046
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
15884
17047
|
const target = targetRaw ? {
|
|
15885
|
-
appId:
|
|
15886
|
-
environment:
|
|
17048
|
+
appId: text10(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
17049
|
+
environment: text10(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
15887
17050
|
} : void 0;
|
|
15888
17051
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
15889
17052
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15971,17 +17134,17 @@ async function builderLibraryDigest(document) {
|
|
|
15971
17134
|
var control2 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
|
|
15972
17135
|
var local3 = (row2) => row2?.document && typeof row2.document === "object" ? row2 : null;
|
|
15973
17136
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
15974
|
-
const
|
|
15975
|
-
return Array.isArray(
|
|
17137
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
17138
|
+
return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
|
|
15976
17139
|
}
|
|
15977
17140
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
15978
|
-
const
|
|
15979
|
-
return control2(Array.isArray(
|
|
17141
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
17142
|
+
return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
15980
17143
|
}
|
|
15981
17144
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
15982
17145
|
const id7 = `builder-library-local:${environment}`;
|
|
15983
|
-
const
|
|
15984
|
-
return local3(Array.isArray(
|
|
17146
|
+
const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
|
|
17147
|
+
return local3(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
15985
17148
|
}
|
|
15986
17149
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
15987
17150
|
const { document, digest } = envelope;
|
|
@@ -17624,8 +18787,8 @@ var readable = /* @__PURE__ */ new Set([
|
|
|
17624
18787
|
"networkCommercialEvents",
|
|
17625
18788
|
"networkCommercialExceptions"
|
|
17626
18789
|
]);
|
|
17627
|
-
var check = (ns,
|
|
17628
|
-
if (!
|
|
18790
|
+
var check = (ns, allowed) => {
|
|
18791
|
+
if (!allowed.has(ns)) throw new TypeError("namespace outside membership authority contract");
|
|
17629
18792
|
return physical(ns);
|
|
17630
18793
|
};
|
|
17631
18794
|
function createMembershipAuthorityStorage(db, secretNames) {
|
|
@@ -17651,9 +18814,9 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
17651
18814
|
const guards = opts?.guards?.map((guard) => ({ ...guard, ns: check(guard.ns, readable) }));
|
|
17652
18815
|
return db.transact(mapped, { ...opts, ...guards ? { guards } : {} });
|
|
17653
18816
|
},
|
|
17654
|
-
secrets: { get(
|
|
17655
|
-
if (!secrets.has(
|
|
17656
|
-
return db.secrets.get(
|
|
18817
|
+
secrets: { get(name2) {
|
|
18818
|
+
if (!secrets.has(name2)) return Promise.reject(new TypeError("vault slot outside membership authority contract"));
|
|
18819
|
+
return db.secrets.get(name2);
|
|
17657
18820
|
} }
|
|
17658
18821
|
};
|
|
17659
18822
|
}
|
|
@@ -17823,7 +18986,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
|
|
|
17823
18986
|
const origin = new URL(options.origin), limit = options.limit ?? 100;
|
|
17824
18987
|
if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash || !Number.isSafeInteger(limit) || limit < 1 || limit > 2e3) throw new TypeError("invalid approval recovery configuration");
|
|
17825
18988
|
const db = ctx.makeDb(env);
|
|
17826
|
-
const
|
|
18989
|
+
const rows6 = (await db.query({ applications: { $: {
|
|
17827
18990
|
where: {
|
|
17828
18991
|
groupId: ctx.chapter.id,
|
|
17829
18992
|
stripeRuntime: env.ODLA_RUNTIME ?? "default",
|
|
@@ -17834,7 +18997,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
|
|
|
17834
18997
|
limit: limit + 1
|
|
17835
18998
|
} } })).applications ?? [];
|
|
17836
18999
|
let repaired = 0, pending = 0;
|
|
17837
|
-
for (const app of
|
|
19000
|
+
for (const app of rows6.slice(0, limit)) {
|
|
17838
19001
|
if (app.groupId !== ctx.chapter.id || app.stripeRuntime !== (env.ODLA_RUNTIME ?? "default") || app.status !== "approved" || app.approvalEffectsPending !== true) throw new Error("approval recovery scope mismatch");
|
|
17839
19002
|
try {
|
|
17840
19003
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { approvalEffectsLastAttemptAt: Date.now() } }], {
|
|
@@ -17859,7 +19022,7 @@ async function retryChapterApprovalEffects(ctx, env, options) {
|
|
|
17859
19022
|
pending++;
|
|
17860
19023
|
}
|
|
17861
19024
|
}
|
|
17862
|
-
return { repaired, pending, more:
|
|
19025
|
+
return { repaired, pending, more: rows6.length > limit };
|
|
17863
19026
|
}
|
|
17864
19027
|
|
|
17865
19028
|
// src/membership-chapter-delivery.ts
|
|
@@ -17875,7 +19038,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17875
19038
|
};
|
|
17876
19039
|
signal.addEventListener("abort", cancel, { once: true });
|
|
17877
19040
|
let size = 0;
|
|
17878
|
-
let
|
|
19041
|
+
let text11 = "";
|
|
17879
19042
|
const decoder = new TextDecoder();
|
|
17880
19043
|
try {
|
|
17881
19044
|
while (true) {
|
|
@@ -17885,10 +19048,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17885
19048
|
if (done) break;
|
|
17886
19049
|
size += value.byteLength;
|
|
17887
19050
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
17888
|
-
|
|
19051
|
+
text11 += decoder.decode(value, { stream: true });
|
|
17889
19052
|
}
|
|
17890
19053
|
try {
|
|
17891
|
-
return JSON.parse(
|
|
19054
|
+
return JSON.parse(text11 + decoder.decode());
|
|
17892
19055
|
} catch {
|
|
17893
19056
|
throw new TypeError(`invalid ${label}`);
|
|
17894
19057
|
}
|
|
@@ -17901,14 +19064,14 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
17901
19064
|
}
|
|
17902
19065
|
|
|
17903
19066
|
// src/membership-chapter-delivery.ts
|
|
17904
|
-
var
|
|
19067
|
+
var import_crm27 = require("@odla-ai/crm");
|
|
17905
19068
|
|
|
17906
19069
|
// src/invitation-test-delivery.ts
|
|
17907
19070
|
function invitationDelivery(env, recipientEmail) {
|
|
17908
19071
|
const normal = { sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL };
|
|
17909
19072
|
if (env.ODLA_ENV !== "dev" || !env.SEND_INVITATION_TEST) return normal;
|
|
17910
|
-
const
|
|
17911
|
-
if (!
|
|
19073
|
+
const allowed = (env.ODLA_INVITATION_TEST_RECIPIENTS ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
19074
|
+
if (!allowed.includes(recipientEmail.trim().toLowerCase())) return normal;
|
|
17912
19075
|
const to = env.ODLA_INVITATION_TEST_TO?.trim();
|
|
17913
19076
|
const from = env.ODLA_INVITATION_TEST_FROM?.trim();
|
|
17914
19077
|
const valid = (value) => value && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(value);
|
|
@@ -17966,7 +19129,7 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
17966
19129
|
join.searchParams.set("seat", effect.seatId);
|
|
17967
19130
|
join.searchParams.set("tier", effect.skuId);
|
|
17968
19131
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
17969
|
-
if (template && !(0,
|
|
19132
|
+
if (template && !(0, import_crm27.validateEmailCopy)(template).length && !template.text.includes("{{purchaserName}}")) {
|
|
17970
19133
|
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: {
|
|
17971
19134
|
...template,
|
|
17972
19135
|
text: "{{purchaserName}} has purchased a membership for you at {{chapterName}}.\n\n" + template.text
|
|
@@ -18428,27 +19591,27 @@ function validatedTokens(input) {
|
|
|
18428
19591
|
}
|
|
18429
19592
|
const tokens = {};
|
|
18430
19593
|
let serializedLength = 0;
|
|
18431
|
-
for (const [
|
|
18432
|
-
if (!TOKEN_NAME.test(
|
|
18433
|
-
throw new TypeError(`signup-control theme token "${
|
|
19594
|
+
for (const [name2, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
19595
|
+
if (!TOKEN_NAME.test(name2)) {
|
|
19596
|
+
throw new TypeError(`signup-control theme token "${name2}" must be an allowlisted --ui-* property`);
|
|
18434
19597
|
}
|
|
18435
19598
|
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
18436
|
-
throw new TypeError(`signup-control theme token "${
|
|
19599
|
+
throw new TypeError(`signup-control theme token "${name2}" must be a finite string or number`);
|
|
18437
19600
|
}
|
|
18438
19601
|
const value = String(raw2).trim();
|
|
18439
19602
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
18440
|
-
throw new TypeError(`signup-control theme token "${
|
|
19603
|
+
throw new TypeError(`signup-control theme token "${name2}" has an unsafe or oversized value`);
|
|
18441
19604
|
}
|
|
18442
|
-
serializedLength +=
|
|
19605
|
+
serializedLength += name2.length + value.length + 4;
|
|
18443
19606
|
if (serializedLength > MAX_SERIALIZED_LENGTH) {
|
|
18444
19607
|
throw new TypeError("signup-control theme exceeds the serialized size bound");
|
|
18445
19608
|
}
|
|
18446
|
-
tokens[
|
|
19609
|
+
tokens[name2] = value;
|
|
18447
19610
|
}
|
|
18448
19611
|
return tokens;
|
|
18449
19612
|
}
|
|
18450
19613
|
function cssFor(tokens) {
|
|
18451
|
-
const declarations = Object.entries(tokens).map(([
|
|
19614
|
+
const declarations = Object.entries(tokens).map(([name2, value]) => ` ${name2}: ${value};`);
|
|
18452
19615
|
return declarations.length ? `:root {
|
|
18453
19616
|
${declarations.join("\n")}
|
|
18454
19617
|
}` : "";
|
|
@@ -18563,6 +19726,7 @@ var BUILTIN_ROUTES = [
|
|
|
18563
19726
|
handleFormation,
|
|
18564
19727
|
handleMember,
|
|
18565
19728
|
handleMemberContent,
|
|
19729
|
+
handleMemberPortal,
|
|
18566
19730
|
handleGifts,
|
|
18567
19731
|
handleNamedSeat,
|
|
18568
19732
|
handleNamedSeatRegift,
|