@odla-ai/chapter 0.43.2 → 0.45.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 +41 -0
- package/dist/{copy-context-Bh1EZvCg.d.ts → copy-context-CIxmneHI.d.ts} +42 -0
- package/dist/index.cjs +795 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -3
- package/dist/index.d.ts +155 -3
- package/dist/index.js +795 -275
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +1476 -287
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +42 -0
- package/dist/worker/index.d.ts +42 -0
- package/dist/worker/index.js +1476 -287
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
|
4
4
|
|
|
5
5
|
// src/auth.ts
|
|
6
6
|
function roleFromClaim(payload, auth) {
|
|
7
|
-
const
|
|
8
|
-
return typeof
|
|
7
|
+
const raw2 = payload[auth.claim];
|
|
8
|
+
return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
|
|
9
9
|
}
|
|
10
10
|
function isAdminRole(role, auth) {
|
|
11
11
|
return role === auth.adminRole;
|
|
@@ -188,7 +188,7 @@ function resolveApplication(a) {
|
|
|
188
188
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
const duplicate = [...required, ...optional].find((field,
|
|
191
|
+
const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
192
192
|
if (duplicate) {
|
|
193
193
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
194
194
|
}
|
|
@@ -336,8 +336,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
336
336
|
function byOrder(a, b) {
|
|
337
337
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
338
338
|
}
|
|
339
|
-
function resolveTiers(group,
|
|
340
|
-
const stored =
|
|
339
|
+
function resolveTiers(group, rows6 = []) {
|
|
340
|
+
const stored = rows6.filter((tier) => tier.active);
|
|
341
341
|
if (stored.length) return [...stored].sort(byOrder);
|
|
342
342
|
return [{
|
|
343
343
|
id: "standard",
|
|
@@ -383,15 +383,15 @@ function rowToTier(row, groupId) {
|
|
|
383
383
|
}
|
|
384
384
|
async function loadTiers(db, group) {
|
|
385
385
|
const groupId = String(group.id ?? "");
|
|
386
|
-
let
|
|
386
|
+
let rows6;
|
|
387
387
|
try {
|
|
388
|
-
|
|
388
|
+
rows6 = (await db.query({
|
|
389
389
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
390
390
|
})).tiers;
|
|
391
391
|
} catch {
|
|
392
|
-
|
|
392
|
+
rows6 = void 0;
|
|
393
393
|
}
|
|
394
|
-
const list = Array.isArray(
|
|
394
|
+
const list = Array.isArray(rows6) ? rows6.map((row) => rowToTier(row, groupId)) : [];
|
|
395
395
|
return resolveTiers(group, list);
|
|
396
396
|
}
|
|
397
397
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -528,8 +528,8 @@ function canonicalSignupControl(value) {
|
|
|
528
528
|
return canonicalValue(value);
|
|
529
529
|
}
|
|
530
530
|
async function signupControlDigest(document) {
|
|
531
|
-
const
|
|
532
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
531
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
532
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
// src/admission-grants.ts
|
|
@@ -565,24 +565,24 @@ function validateAdmissionGrantDocument(input) {
|
|
|
565
565
|
const grants = [];
|
|
566
566
|
const seenIds = /* @__PURE__ */ new Set();
|
|
567
567
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
568
|
-
for (const [
|
|
569
|
-
const grant =
|
|
568
|
+
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
569
|
+
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
570
570
|
const id = typeof grant.id === "string" ? grant.id : "";
|
|
571
571
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
572
572
|
const normalized = normalizeAdmissionEmail(email);
|
|
573
573
|
const decision = grant.decision;
|
|
574
574
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
575
575
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
576
|
-
if (!ID.test(id)) errors.push(`document.grants[${
|
|
576
|
+
if (!ID.test(id)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
577
577
|
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
578
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${
|
|
578
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
579
579
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
580
580
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
581
|
-
errors.push(`document.grants[${
|
|
581
|
+
errors.push(`document.grants[${index2}].decision is invalid`);
|
|
582
582
|
}
|
|
583
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${
|
|
583
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
|
|
584
584
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
585
|
-
errors.push(`document.grants[${
|
|
585
|
+
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
586
586
|
}
|
|
587
587
|
seenIds.add(id);
|
|
588
588
|
seenEmails.add(normalized);
|
|
@@ -650,7 +650,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
|
|
|
650
650
|
return row ? row : null;
|
|
651
651
|
}
|
|
652
652
|
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
653
|
-
const { document, digest } = envelope;
|
|
653
|
+
const { document, digest: digest2 } = envelope;
|
|
654
654
|
const current = rows((await db.query({ admissionGrants: { $: {
|
|
655
655
|
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
656
656
|
limit: MAX_GRANTS
|
|
@@ -702,11 +702,11 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
702
702
|
targetId: document.target.chapterId,
|
|
703
703
|
environment: document.target.environment,
|
|
704
704
|
revision: document.revision,
|
|
705
|
-
digest,
|
|
705
|
+
digest: digest2,
|
|
706
706
|
active: true,
|
|
707
707
|
receivedAt
|
|
708
708
|
} });
|
|
709
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${
|
|
709
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest2}` });
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
// src/flow-fields.ts
|
|
@@ -724,8 +724,8 @@ var APPLICATION_LABELS = {
|
|
|
724
724
|
state: "State"
|
|
725
725
|
};
|
|
726
726
|
function humanize(id) {
|
|
727
|
-
const
|
|
728
|
-
return
|
|
727
|
+
const text5 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
728
|
+
return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id;
|
|
729
729
|
}
|
|
730
730
|
function applicationDefs(chapter) {
|
|
731
731
|
try {
|
|
@@ -740,7 +740,7 @@ function applicationType(id, def) {
|
|
|
740
740
|
}
|
|
741
741
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
742
742
|
const defs = applicationDefs(chapter);
|
|
743
|
-
return [...application.required, ...application.optional].map((id,
|
|
743
|
+
return [...application.required, ...application.optional].map((id, order2) => {
|
|
744
744
|
const def = defs[id];
|
|
745
745
|
const conditions = application.conditions[id];
|
|
746
746
|
const type = applicationType(id, def);
|
|
@@ -749,7 +749,7 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
749
749
|
label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
|
|
750
750
|
type,
|
|
751
751
|
required: application.required.includes(id),
|
|
752
|
-
order,
|
|
752
|
+
order: order2,
|
|
753
753
|
maxLength: application.maxLen[id] ?? application.defaultMaxLen,
|
|
754
754
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
755
755
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
@@ -760,14 +760,14 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
760
760
|
function resolveFormationFlowFields(crm, formation) {
|
|
761
761
|
if (!formation.enabled) return [];
|
|
762
762
|
const def = crm.type(formation.type);
|
|
763
|
-
return [...formation.required, ...formation.optional].map((id,
|
|
763
|
+
return [...formation.required, ...formation.optional].map((id, order2) => {
|
|
764
764
|
const field = def.fields[id];
|
|
765
765
|
return {
|
|
766
766
|
id,
|
|
767
767
|
label: field.label ?? humanize(id),
|
|
768
768
|
type: field.type,
|
|
769
769
|
required: formation.required.includes(id),
|
|
770
|
-
order,
|
|
770
|
+
order: order2,
|
|
771
771
|
maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
|
|
772
772
|
...field.options ? { options: [...field.options] } : {},
|
|
773
773
|
...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -811,9 +811,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
811
811
|
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
812
812
|
const ids = /* @__PURE__ */ new Set();
|
|
813
813
|
const offers = [];
|
|
814
|
-
for (const [
|
|
815
|
-
const path = `document.giftOffers.offers[${
|
|
816
|
-
const item = record(
|
|
814
|
+
for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
815
|
+
const path = `document.giftOffers.offers[${index2}]`;
|
|
816
|
+
const item = record(raw2);
|
|
817
817
|
if (!item) {
|
|
818
818
|
errors.push(`${path} must be an object`);
|
|
819
819
|
continue;
|
|
@@ -975,8 +975,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
975
975
|
const typeKey = type.toLowerCase();
|
|
976
976
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
977
977
|
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
978
|
-
const
|
|
979
|
-
if (readable &&
|
|
978
|
+
const raw2 = `${prefix}:${hubRecordId}`;
|
|
979
|
+
if (readable && raw2.length <= 64) return raw2;
|
|
980
980
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
981
981
|
}
|
|
982
982
|
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
@@ -1260,10 +1260,10 @@ function planDelivery(input) {
|
|
|
1260
1260
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1261
1261
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1262
1262
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1263
|
-
const
|
|
1263
|
+
const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1264
1264
|
|
|
1265
1265
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1266
|
-
return { deliver: true, transport, to, subject, text:
|
|
1266
|
+
return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
|
|
1267
1267
|
}
|
|
1268
1268
|
|
|
1269
1269
|
// src/notify.ts
|
|
@@ -1450,27 +1450,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1450
1450
|
if (db.aggregate) {
|
|
1451
1451
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1452
1452
|
}
|
|
1453
|
-
const
|
|
1454
|
-
return Array.isArray(
|
|
1453
|
+
const rows6 = (await db.query({ signupControls: {} })).signupControls;
|
|
1454
|
+
return Array.isArray(rows6) && rows6.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1455
1455
|
}
|
|
1456
1456
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1457
1457
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1458
|
-
const
|
|
1458
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
1459
1459
|
where: { targetId, environment },
|
|
1460
1460
|
order: { revision: "desc" },
|
|
1461
1461
|
limit: 1
|
|
1462
1462
|
} } })).signupControls;
|
|
1463
|
-
const latest = Array.isArray(
|
|
1463
|
+
const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1464
1464
|
return asStored(latest);
|
|
1465
1465
|
}
|
|
1466
1466
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1467
1467
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1468
|
-
const
|
|
1468
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
1469
1469
|
where: { targetId, environment, active: true },
|
|
1470
1470
|
order: { revision: "desc" },
|
|
1471
1471
|
limit: 1
|
|
1472
1472
|
} } })).signupControls;
|
|
1473
|
-
const active = Array.isArray(
|
|
1473
|
+
const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1474
1474
|
return asStored(active);
|
|
1475
1475
|
}
|
|
1476
1476
|
function controlledApplication(base, document) {
|
|
@@ -1499,7 +1499,7 @@ var byPublicId = (ns, value) => ({
|
|
|
1499
1499
|
value
|
|
1500
1500
|
});
|
|
1501
1501
|
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
1502
|
-
const { document, digest } = envelope;
|
|
1502
|
+
const { document, digest: digest2 } = envelope;
|
|
1503
1503
|
const currentRows = (await db.query({ tiers: { $: {
|
|
1504
1504
|
where: { groupId: document.target.chapterId },
|
|
1505
1505
|
limit: 100
|
|
@@ -1550,12 +1550,12 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
1550
1550
|
targetId: document.target.chapterId,
|
|
1551
1551
|
environment: document.target.environment,
|
|
1552
1552
|
revision: document.revision,
|
|
1553
|
-
digest,
|
|
1553
|
+
digest: digest2,
|
|
1554
1554
|
document,
|
|
1555
1555
|
active: true,
|
|
1556
1556
|
receivedAt
|
|
1557
1557
|
} });
|
|
1558
|
-
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${
|
|
1558
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest2}` });
|
|
1559
1559
|
}
|
|
1560
1560
|
|
|
1561
1561
|
// src/session.ts
|
|
@@ -1995,11 +1995,11 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1995
1995
|
return json({ error: `application cannot resume from status "${String(app.status ?? "")}"` }, 409);
|
|
1996
1996
|
}
|
|
1997
1997
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
1998
|
-
const
|
|
1999
|
-
if (
|
|
1998
|
+
const raw2 = await req.text();
|
|
1999
|
+
if (raw2.length > chapter.application.bodyCap) return json({ error: "request body too large" }, 413);
|
|
2000
2000
|
let parsed;
|
|
2001
2001
|
try {
|
|
2002
|
-
parsed = JSON.parse(
|
|
2002
|
+
parsed = JSON.parse(raw2);
|
|
2003
2003
|
} catch {
|
|
2004
2004
|
return json({ error: "invalid JSON body" }, 400);
|
|
2005
2005
|
}
|
|
@@ -2056,23 +2056,23 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
2056
2056
|
const db = ctx.makeDb(env);
|
|
2057
2057
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2058
2058
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2059
|
-
let
|
|
2059
|
+
let raw2;
|
|
2060
2060
|
let sourceId;
|
|
2061
2061
|
if (req.headers.has(FEDERATION_HEADERS.version)) {
|
|
2062
2062
|
const verified = await verifyFederatedRequest(req, { secret });
|
|
2063
2063
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2064
|
-
|
|
2064
|
+
raw2 = verified.body;
|
|
2065
2065
|
sourceId = verified.sender;
|
|
2066
2066
|
} else {
|
|
2067
2067
|
const provided = (req.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
2068
2068
|
if (provided.length !== secret.length || provided !== secret) {
|
|
2069
2069
|
return json({ error: "unauthorized" }, 401);
|
|
2070
2070
|
}
|
|
2071
|
-
|
|
2071
|
+
raw2 = await req.text();
|
|
2072
2072
|
}
|
|
2073
2073
|
let payload;
|
|
2074
2074
|
try {
|
|
2075
|
-
payload = JSON.parse(
|
|
2075
|
+
payload = JSON.parse(raw2);
|
|
2076
2076
|
} catch {
|
|
2077
2077
|
return json({ error: "invalid JSON body" }, 400);
|
|
2078
2078
|
}
|
|
@@ -2151,10 +2151,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2151
2151
|
errors.push(`${path} must be a string`);
|
|
2152
2152
|
return void 0;
|
|
2153
2153
|
}
|
|
2154
|
-
const
|
|
2155
|
-
if (required && !
|
|
2156
|
-
if (
|
|
2157
|
-
return
|
|
2154
|
+
const text5 = value.trim();
|
|
2155
|
+
if (required && !text5) errors.push(`${path} is required`);
|
|
2156
|
+
if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2157
|
+
return text5;
|
|
2158
2158
|
}
|
|
2159
2159
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2160
2160
|
const errors = [];
|
|
@@ -2172,9 +2172,9 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2172
2172
|
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
2173
2173
|
const seen = /* @__PURE__ */ new Set();
|
|
2174
2174
|
const tiers = [];
|
|
2175
|
-
for (const [
|
|
2176
|
-
const path = `document.tiers[${
|
|
2177
|
-
const item = objectRecord(
|
|
2175
|
+
for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
2176
|
+
const path = `document.tiers[${index2}]`;
|
|
2177
|
+
const item = objectRecord(raw2);
|
|
2178
2178
|
if (!item) {
|
|
2179
2179
|
errors.push(`${path} must be an object`);
|
|
2180
2180
|
continue;
|
|
@@ -2203,8 +2203,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2203
2203
|
else {
|
|
2204
2204
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2205
2205
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2206
|
-
const
|
|
2207
|
-
return
|
|
2206
|
+
const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2207
|
+
return text5 ? [text5] : [];
|
|
2208
2208
|
});
|
|
2209
2209
|
}
|
|
2210
2210
|
}
|
|
@@ -2313,9 +2313,9 @@ function tiersOf(value, errors) {
|
|
|
2313
2313
|
}
|
|
2314
2314
|
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
2315
2315
|
const seen = /* @__PURE__ */ new Set();
|
|
2316
|
-
return value.slice(0, 20).flatMap((
|
|
2317
|
-
const path = `document.tiers[${
|
|
2318
|
-
const item = recordOf(
|
|
2316
|
+
return value.slice(0, 20).flatMap((raw2, index2) => {
|
|
2317
|
+
const path = `document.tiers[${index2}]`;
|
|
2318
|
+
const item = recordOf(raw2);
|
|
2319
2319
|
if (!item) {
|
|
2320
2320
|
errors.push(`${path} must be an object`);
|
|
2321
2321
|
return [];
|
|
@@ -2442,7 +2442,7 @@ function validateSignupControlDocument(input) {
|
|
|
2442
2442
|
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
2443
2443
|
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
2444
2444
|
if (!page.ok) errors.push(...page.errors);
|
|
2445
|
-
else if (page.value.tiers.some((tier,
|
|
2445
|
+
else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
|
|
2446
2446
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
2447
2447
|
}
|
|
2448
2448
|
const application = applicationOf(root.application, errors);
|
|
@@ -2490,7 +2490,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2490
2490
|
} catch {
|
|
2491
2491
|
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
2492
2492
|
}
|
|
2493
|
-
const
|
|
2493
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
2494
2494
|
const checked = validateSignupControlDocument(input.document);
|
|
2495
2495
|
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
2496
2496
|
const document = checked.value;
|
|
@@ -2500,11 +2500,11 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2500
2500
|
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
2501
2501
|
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
2502
2502
|
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
2503
|
-
if (!
|
|
2503
|
+
if (!digest2 || await signupControlDigest(document) !== digest2) return reject("invalid_digest", 400);
|
|
2504
2504
|
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2505
2505
|
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
2506
2506
|
if (latest && document.revision === latest.revision) {
|
|
2507
|
-
if (latest.digest !==
|
|
2507
|
+
if (latest.digest !== digest2) return reject("conflicting_revision", 409);
|
|
2508
2508
|
return json({
|
|
2509
2509
|
ok: true,
|
|
2510
2510
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -2516,7 +2516,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2516
2516
|
}
|
|
2517
2517
|
const receivedAt = Date.now();
|
|
2518
2518
|
try {
|
|
2519
|
-
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
2519
|
+
await materializeSignupControl(db, { digest: digest2, document }, latest, receivedAt);
|
|
2520
2520
|
} catch {
|
|
2521
2521
|
return reject("materialization_failed", 503);
|
|
2522
2522
|
}
|
|
@@ -2524,7 +2524,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2524
2524
|
ok: true,
|
|
2525
2525
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
2526
2526
|
revision: document.revision,
|
|
2527
|
-
digest,
|
|
2527
|
+
digest: digest2,
|
|
2528
2528
|
status: "accepted",
|
|
2529
2529
|
acknowledgedAt: receivedAt
|
|
2530
2530
|
});
|
|
@@ -2552,8 +2552,8 @@ function canonicalMemberContent(value) {
|
|
|
2552
2552
|
return canonicalValue2(value);
|
|
2553
2553
|
}
|
|
2554
2554
|
async function memberContentDigest(document) {
|
|
2555
|
-
const
|
|
2556
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
2555
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2556
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2557
2557
|
}
|
|
2558
2558
|
|
|
2559
2559
|
// src/member-content-validation-helpers.ts
|
|
@@ -2590,21 +2590,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
|
2590
2590
|
return valueText;
|
|
2591
2591
|
}
|
|
2592
2592
|
function offerValue(value, path, errors, tiers, audience) {
|
|
2593
|
-
const
|
|
2594
|
-
if (!
|
|
2593
|
+
const raw2 = record2(value);
|
|
2594
|
+
if (!raw2) {
|
|
2595
2595
|
errors.push(`${path} must be an object`);
|
|
2596
2596
|
return void 0;
|
|
2597
2597
|
}
|
|
2598
|
-
unknownKeys2(
|
|
2599
|
-
const tierId = textValue(
|
|
2598
|
+
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
2599
|
+
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2600
2600
|
if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2601
2601
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2602
2602
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2603
2603
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
2604
2604
|
}
|
|
2605
|
-
const label = textValue(
|
|
2606
|
-
const bodyMarkdown = textValue(
|
|
2607
|
-
const ctaLabel = textValue(
|
|
2605
|
+
const label = textValue(raw2.label, `${path}.label`, errors);
|
|
2606
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
|
|
2607
|
+
const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2608
2608
|
return {
|
|
2609
2609
|
tierId,
|
|
2610
2610
|
...label ? { label } : {},
|
|
@@ -2656,37 +2656,37 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2656
2656
|
const ids = /* @__PURE__ */ new Set();
|
|
2657
2657
|
const slugs = /* @__PURE__ */ new Set();
|
|
2658
2658
|
const pages = [];
|
|
2659
|
-
for (const [
|
|
2660
|
-
const path = `document.pages[${
|
|
2661
|
-
const
|
|
2662
|
-
if (!
|
|
2659
|
+
for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2660
|
+
const path = `document.pages[${index2}]`;
|
|
2661
|
+
const raw2 = record2(value);
|
|
2662
|
+
if (!raw2) {
|
|
2663
2663
|
errors.push(`${path} must be an object`);
|
|
2664
2664
|
continue;
|
|
2665
2665
|
}
|
|
2666
|
-
unknownKeys2(
|
|
2667
|
-
const id = textValue(
|
|
2668
|
-
const slug = textValue(
|
|
2669
|
-
const title = textValue(
|
|
2666
|
+
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
2667
|
+
const id = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2668
|
+
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2669
|
+
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2670
2670
|
if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2671
2671
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2672
2672
|
if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
|
|
2673
2673
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2674
2674
|
ids.add(id);
|
|
2675
2675
|
slugs.add(slug);
|
|
2676
|
-
const navRaw = record2(
|
|
2677
|
-
let navigation = { section: "General", order:
|
|
2676
|
+
const navRaw = record2(raw2.navigation);
|
|
2677
|
+
let navigation = { section: "General", order: index2 };
|
|
2678
2678
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2679
2679
|
else {
|
|
2680
2680
|
unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2681
2681
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2682
|
-
const
|
|
2683
|
-
if (!Number.isInteger(
|
|
2682
|
+
const order2 = navRaw.order;
|
|
2683
|
+
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
2684
2684
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2685
2685
|
}
|
|
2686
2686
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2687
|
-
navigation = { section, order: Number.isInteger(
|
|
2687
|
+
navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
|
|
2688
2688
|
}
|
|
2689
|
-
const audienceRaw = record2(
|
|
2689
|
+
const audienceRaw = record2(raw2.audience);
|
|
2690
2690
|
let audience = { kind: "all-members" };
|
|
2691
2691
|
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2692
2692
|
else {
|
|
@@ -2708,12 +2708,12 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2708
2708
|
audience = { kind: "tiers", tierIds: normalized };
|
|
2709
2709
|
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2710
2710
|
}
|
|
2711
|
-
const summary = textValue(
|
|
2712
|
-
const teaserMarkdown = textValue(
|
|
2713
|
-
const bodyMarkdown = textValue(
|
|
2711
|
+
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
2712
|
+
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
|
|
2713
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
|
|
2714
2714
|
let upsell;
|
|
2715
|
-
if (
|
|
2716
|
-
const upsellRaw = record2(
|
|
2715
|
+
if (raw2.upsell !== void 0) {
|
|
2716
|
+
const upsellRaw = record2(raw2.upsell);
|
|
2717
2717
|
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2718
2718
|
else {
|
|
2719
2719
|
unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
@@ -2730,10 +2730,10 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2730
2730
|
pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2731
2731
|
}
|
|
2732
2732
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2733
|
-
for (const [
|
|
2733
|
+
for (const [index2, page] of pages.entries()) {
|
|
2734
2734
|
const parentId = page.navigation.parentId;
|
|
2735
|
-
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${
|
|
2736
|
-
if (parentId === page.id) errors.push(`document.pages[${
|
|
2735
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
|
|
2736
|
+
if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
|
|
2737
2737
|
}
|
|
2738
2738
|
for (const page of pages) {
|
|
2739
2739
|
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
@@ -2754,21 +2754,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2754
2754
|
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2755
2755
|
const seen = /* @__PURE__ */ new Set();
|
|
2756
2756
|
upsellOverrides = [];
|
|
2757
|
-
for (const [
|
|
2758
|
-
const path = `document.upsellOverrides[${
|
|
2759
|
-
const
|
|
2760
|
-
if (!
|
|
2757
|
+
for (const [index2, value] of values.entries()) {
|
|
2758
|
+
const path = `document.upsellOverrides[${index2}]`;
|
|
2759
|
+
const raw2 = record2(value);
|
|
2760
|
+
if (!raw2) {
|
|
2761
2761
|
errors.push(`${path} must be an object`);
|
|
2762
2762
|
continue;
|
|
2763
2763
|
}
|
|
2764
|
-
unknownKeys2(
|
|
2765
|
-
const pageId = textValue(
|
|
2764
|
+
unknownKeys2(raw2, OVERRIDE_KEYS, path, errors);
|
|
2765
|
+
const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2766
2766
|
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2767
2767
|
seen.add(pageId);
|
|
2768
2768
|
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2769
2769
|
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2770
2770
|
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2771
|
-
const offer = offerValue(
|
|
2771
|
+
const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2772
2772
|
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2773
2773
|
}
|
|
2774
2774
|
}
|
|
@@ -2846,36 +2846,36 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2846
2846
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2847
2847
|
async function exists(db, ns, where) {
|
|
2848
2848
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2849
|
-
const
|
|
2850
|
-
return
|
|
2849
|
+
const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2850
|
+
return rows6.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2851
2851
|
}
|
|
2852
2852
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2853
2853
|
const where = { targetId, environment, active: true };
|
|
2854
2854
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2855
|
-
const
|
|
2855
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
2856
2856
|
where,
|
|
2857
2857
|
order: { revision: "desc" },
|
|
2858
2858
|
limit: 1
|
|
2859
2859
|
} } })).memberContentControls;
|
|
2860
|
-
return control(Array.isArray(
|
|
2860
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2861
2861
|
}
|
|
2862
2862
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2863
2863
|
const where = { targetId, environment };
|
|
2864
2864
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2865
|
-
const
|
|
2865
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
2866
2866
|
where,
|
|
2867
2867
|
order: { revision: "desc" },
|
|
2868
2868
|
limit: 1
|
|
2869
2869
|
} } })).memberContentControls;
|
|
2870
|
-
return control(Array.isArray(
|
|
2870
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2871
2871
|
}
|
|
2872
2872
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2873
2873
|
const id = `member-content-local:${environment}`;
|
|
2874
|
-
const
|
|
2875
|
-
return local(Array.isArray(
|
|
2874
|
+
const rows6 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2875
|
+
return local(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
2876
2876
|
}
|
|
2877
2877
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2878
|
-
const { document, digest } = envelope;
|
|
2878
|
+
const { document, digest: digest2 } = envelope;
|
|
2879
2879
|
const target = document.target;
|
|
2880
2880
|
const id = `member-content-control:${target.environment}:${document.revision}`;
|
|
2881
2881
|
const ops = [];
|
|
@@ -2888,12 +2888,12 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
2888
2888
|
targetId: target.chapterId,
|
|
2889
2889
|
environment: target.environment,
|
|
2890
2890
|
revision: document.revision,
|
|
2891
|
-
digest,
|
|
2891
|
+
digest: digest2,
|
|
2892
2892
|
document,
|
|
2893
2893
|
active: true,
|
|
2894
2894
|
receivedAt
|
|
2895
2895
|
} });
|
|
2896
|
-
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${
|
|
2896
|
+
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest2}` });
|
|
2897
2897
|
}
|
|
2898
2898
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
2899
2899
|
const id = `member-content-local:${environment}`;
|
|
@@ -2927,12 +2927,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2927
2927
|
}
|
|
2928
2928
|
async function memberTierId(db, chapterId, email) {
|
|
2929
2929
|
if (!email) return null;
|
|
2930
|
-
let
|
|
2930
|
+
let rows6 = (await db.query({ applications: { $: {
|
|
2931
2931
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2932
2932
|
limit: 20
|
|
2933
2933
|
} } })).applications;
|
|
2934
|
-
if (!
|
|
2935
|
-
const latest = Array.isArray(
|
|
2934
|
+
if (!rows6?.length) rows6 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2935
|
+
const latest = Array.isArray(rows6) ? [...rows6].filter((row) => String(row.groupId ?? chapterId) === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
|
|
2936
2936
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2937
2937
|
}
|
|
2938
2938
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -2957,7 +2957,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
2957
2957
|
} catch {
|
|
2958
2958
|
return reject2("invalid_document", 400, ["request body must be JSON"]);
|
|
2959
2959
|
}
|
|
2960
|
-
const
|
|
2960
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
2961
2961
|
const initial = validateMemberContentDocument(input.document, {
|
|
2962
2962
|
sourceType: "parent"
|
|
2963
2963
|
});
|
|
@@ -2985,11 +2985,11 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
2985
2985
|
return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
|
|
2986
2986
|
}
|
|
2987
2987
|
}
|
|
2988
|
-
if (!
|
|
2988
|
+
if (!digest2 || await memberContentDigest(document) !== digest2) return reject2("invalid_digest", 400);
|
|
2989
2989
|
const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2990
2990
|
if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
|
|
2991
2991
|
if (latest && document.revision === latest.revision) {
|
|
2992
|
-
if (latest.digest !==
|
|
2992
|
+
if (latest.digest !== digest2) return reject2("conflicting_revision", 409);
|
|
2993
2993
|
return json({
|
|
2994
2994
|
ok: true,
|
|
2995
2995
|
target: document.target,
|
|
@@ -3001,7 +3001,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3001
3001
|
}
|
|
3002
3002
|
const receivedAt = Date.now();
|
|
3003
3003
|
try {
|
|
3004
|
-
await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
|
|
3004
|
+
await materializeMemberContentControl(db, { digest: digest2, document }, latest, receivedAt);
|
|
3005
3005
|
} catch {
|
|
3006
3006
|
return reject2("materialization_failed", 503);
|
|
3007
3007
|
}
|
|
@@ -3009,7 +3009,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3009
3009
|
ok: true,
|
|
3010
3010
|
target: document.target,
|
|
3011
3011
|
revision: document.revision,
|
|
3012
|
-
digest,
|
|
3012
|
+
digest: digest2,
|
|
3013
3013
|
status: "accepted",
|
|
3014
3014
|
acknowledgedAt: receivedAt
|
|
3015
3015
|
});
|
|
@@ -3061,11 +3061,11 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
3061
3061
|
}
|
|
3062
3062
|
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
3063
3063
|
if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
3064
|
-
const
|
|
3065
|
-
if (
|
|
3064
|
+
const raw2 = await req.text();
|
|
3065
|
+
if (raw2.length > BODY_CAP) return json({ error: "request body too large" }, 413);
|
|
3066
3066
|
let input;
|
|
3067
3067
|
try {
|
|
3068
|
-
input = JSON.parse(
|
|
3068
|
+
input = JSON.parse(raw2);
|
|
3069
3069
|
} catch {
|
|
3070
3070
|
return json({ error: "invalid JSON body" }, 400);
|
|
3071
3071
|
}
|
|
@@ -3133,18 +3133,18 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3133
3133
|
} catch {
|
|
3134
3134
|
return reject3("invalid_document", 400, ["request body must be JSON"]);
|
|
3135
3135
|
}
|
|
3136
|
-
const
|
|
3136
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
3137
3137
|
const checked = validateAdmissionGrantDocument(input.document);
|
|
3138
3138
|
if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
|
|
3139
3139
|
const document = checked.value;
|
|
3140
3140
|
if (document.source.appId !== claimedSender) return reject3("wrong_source", 403);
|
|
3141
3141
|
if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
|
|
3142
3142
|
if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
|
|
3143
|
-
if (!
|
|
3143
|
+
if (!digest2 || await signupControlDigest(document) !== digest2) return reject3("invalid_digest", 400);
|
|
3144
3144
|
const latest = await loadLatestAdmissionRevision(db, claimedSender, ctx.chapter.id, env.ODLA_ENV);
|
|
3145
3145
|
if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
|
|
3146
3146
|
if (latest && document.revision === latest.revision) {
|
|
3147
|
-
if (latest.digest !==
|
|
3147
|
+
if (latest.digest !== digest2) return reject3("conflicting_revision", 409);
|
|
3148
3148
|
return json({
|
|
3149
3149
|
ok: true,
|
|
3150
3150
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -3156,7 +3156,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3156
3156
|
}
|
|
3157
3157
|
const receivedAt = Date.now();
|
|
3158
3158
|
try {
|
|
3159
|
-
await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
|
|
3159
|
+
await materializeAdmissionGrants(db, { digest: digest2, document }, latest, receivedAt);
|
|
3160
3160
|
} catch {
|
|
3161
3161
|
return reject3("materialization_failed", 503);
|
|
3162
3162
|
}
|
|
@@ -3164,7 +3164,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3164
3164
|
ok: true,
|
|
3165
3165
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
3166
3166
|
revision: document.revision,
|
|
3167
|
-
digest,
|
|
3167
|
+
digest: digest2,
|
|
3168
3168
|
status: "accepted",
|
|
3169
3169
|
acknowledgedAt: receivedAt
|
|
3170
3170
|
});
|
|
@@ -3367,9 +3367,9 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
3367
3367
|
if (record4.type !== "person") return null;
|
|
3368
3368
|
const field = ctx.chapter.crm.config.types.person?.emailField;
|
|
3369
3369
|
if (!field) return null;
|
|
3370
|
-
const
|
|
3371
|
-
if (typeof
|
|
3372
|
-
const normalized = normalizeAdmissionEmail(
|
|
3370
|
+
const raw2 = record4.fields[field] ?? record4.primaryEmail;
|
|
3371
|
+
if (typeof raw2 !== "string") return null;
|
|
3372
|
+
const normalized = normalizeAdmissionEmail(raw2);
|
|
3373
3373
|
return isValidEmail(normalized) ? normalized : null;
|
|
3374
3374
|
}
|
|
3375
3375
|
async function grantInput(db, ctx, assignment) {
|
|
@@ -3389,7 +3389,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3389
3389
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
3390
3390
|
const id = assignmentId(opts.target.id, opts.personId);
|
|
3391
3391
|
const existing = current.find((row) => row.id === id);
|
|
3392
|
-
const
|
|
3392
|
+
const digest2 = await signupControlDigest({
|
|
3393
3393
|
targetId: opts.target.id,
|
|
3394
3394
|
personId: opts.personId,
|
|
3395
3395
|
decision: opts.decision,
|
|
@@ -3404,7 +3404,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3404
3404
|
} } });
|
|
3405
3405
|
const receipt = admissionRows(priorMutation2.leaderAdmissionMutations)[0];
|
|
3406
3406
|
if (receipt) {
|
|
3407
|
-
if (receipt.requestDigest !==
|
|
3407
|
+
if (receipt.requestDigest !== digest2 || receipt.personId !== opts.personId) {
|
|
3408
3408
|
throw new Error("mutation_conflict");
|
|
3409
3409
|
}
|
|
3410
3410
|
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
@@ -3412,7 +3412,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3412
3412
|
return { delivery: saved, duplicate: true };
|
|
3413
3413
|
}
|
|
3414
3414
|
if (existing?.lastMutationId === opts.mutationId) {
|
|
3415
|
-
if (existing.lastRequestDigest !==
|
|
3415
|
+
if (existing.lastRequestDigest !== digest2) throw new Error("mutation_conflict");
|
|
3416
3416
|
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3417
3417
|
if (!saved) throw new Error("delivery_missing");
|
|
3418
3418
|
return { delivery: saved, duplicate: true };
|
|
@@ -3427,7 +3427,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3427
3427
|
...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {},
|
|
3428
3428
|
active: opts.active,
|
|
3429
3429
|
lastMutationId: opts.mutationId,
|
|
3430
|
-
lastRequestDigest:
|
|
3430
|
+
lastRequestDigest: digest2,
|
|
3431
3431
|
createdAt: existing?.createdAt ?? now,
|
|
3432
3432
|
createdBy: existing?.createdBy ?? opts.actor,
|
|
3433
3433
|
updatedAt: now,
|
|
@@ -3463,7 +3463,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3463
3463
|
id: mutationReceiptId,
|
|
3464
3464
|
targetId: opts.target.id,
|
|
3465
3465
|
personId: opts.personId,
|
|
3466
|
-
requestDigest:
|
|
3466
|
+
requestDigest: digest2,
|
|
3467
3467
|
revision,
|
|
3468
3468
|
createdAt: now
|
|
3469
3469
|
} }
|
|
@@ -3658,8 +3658,8 @@ function makeCalendar(env) {
|
|
|
3658
3658
|
}
|
|
3659
3659
|
async function firstRow(db, ns, q) {
|
|
3660
3660
|
const res = await db.query({ [ns]: { $: q } });
|
|
3661
|
-
const
|
|
3662
|
-
return Array.isArray(
|
|
3661
|
+
const rows6 = res[ns];
|
|
3662
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3663
3663
|
}
|
|
3664
3664
|
async function computeSlots(cal, cfg) {
|
|
3665
3665
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -3916,17 +3916,17 @@ async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
|
3916
3916
|
try {
|
|
3917
3917
|
const group = await groupFor(db, app);
|
|
3918
3918
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
3919
|
-
const
|
|
3919
|
+
const text5 = (value) => typeof value === "string" ? value : "";
|
|
3920
3920
|
await sendTemplated(deps(db, env), {
|
|
3921
3921
|
group: emailGroupFrom(group),
|
|
3922
3922
|
template: "adminNotification",
|
|
3923
3923
|
to: group.notificationEmail,
|
|
3924
3924
|
vars: {
|
|
3925
|
-
firstName:
|
|
3926
|
-
lastName:
|
|
3927
|
-
email:
|
|
3928
|
-
phone:
|
|
3929
|
-
state:
|
|
3925
|
+
firstName: text5(app.firstName),
|
|
3926
|
+
lastName: text5(app.lastName),
|
|
3927
|
+
email: text5(app.email),
|
|
3928
|
+
phone: text5(app.phone),
|
|
3929
|
+
state: text5(app.state)
|
|
3930
3930
|
},
|
|
3931
3931
|
dedupeKey: `${eventId}:admin`,
|
|
3932
3932
|
applicationId: String(app.id)
|
|
@@ -4059,8 +4059,8 @@ function createStripeProvider(config) {
|
|
|
4059
4059
|
const sk = requireSecret(secretKey);
|
|
4060
4060
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4061
4061
|
if (!charges.ok) fail("charges list", charges);
|
|
4062
|
-
const
|
|
4063
|
-
const paid =
|
|
4062
|
+
const rows6 = charges.body.data ?? [];
|
|
4063
|
+
const paid = rows6.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4064
4064
|
const charge = paid[paid.length - 1];
|
|
4065
4065
|
if (!charge) {
|
|
4066
4066
|
const err = new Error("no paid charge to refund");
|
|
@@ -4168,8 +4168,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4168
4168
|
throw new Error("gift issuance conflict");
|
|
4169
4169
|
}
|
|
4170
4170
|
}
|
|
4171
|
-
function buyerGiftClaims(
|
|
4172
|
-
return
|
|
4171
|
+
function buyerGiftClaims(rows6, now) {
|
|
4172
|
+
return rows6.map((row) => {
|
|
4173
4173
|
const stored = String(row.status ?? "pending");
|
|
4174
4174
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4175
4175
|
return {
|
|
@@ -4461,8 +4461,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4461
4461
|
|
|
4462
4462
|
// src/stripe-webhook-partition.ts
|
|
4463
4463
|
var firstRow2 = async (db, ns, q) => {
|
|
4464
|
-
const
|
|
4465
|
-
return Array.isArray(
|
|
4464
|
+
const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4465
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
4466
4466
|
};
|
|
4467
4467
|
var stripeEventDigest = async (rawBody) => {
|
|
4468
4468
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4509,9 +4509,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
4509
4509
|
const raced = await receiptFor(db, input.eventId);
|
|
4510
4510
|
return raced?.digest === input.digest ? "replayed" : "collision";
|
|
4511
4511
|
}
|
|
4512
|
-
async function quarantineStripeEvent(db, eventId,
|
|
4512
|
+
async function quarantineStripeEvent(db, eventId, digest2, event) {
|
|
4513
4513
|
const disposition = "quarantine_unmatched";
|
|
4514
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
4514
|
+
const stored = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition, matched: false });
|
|
4515
4515
|
if (stored === "collision") {
|
|
4516
4516
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4517
4517
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4519,10 +4519,10 @@ async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
|
4519
4519
|
reportStripePartition("warn", eventId, event, disposition);
|
|
4520
4520
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
4521
4521
|
}
|
|
4522
|
-
async function replayedStripeReceipt(db, eventId,
|
|
4522
|
+
async function replayedStripeReceipt(db, eventId, digest2, event) {
|
|
4523
4523
|
const existing = await receiptFor(db, eventId);
|
|
4524
4524
|
if (!existing) return null;
|
|
4525
|
-
if (existing.digest !==
|
|
4525
|
+
if (existing.digest !== digest2) {
|
|
4526
4526
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4527
4527
|
return json({ ok: true, matched: false, collision: true });
|
|
4528
4528
|
}
|
|
@@ -4532,8 +4532,8 @@ async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
|
4532
4532
|
// src/worker-routes-payments.ts
|
|
4533
4533
|
async function firstRow3(db, ns, q) {
|
|
4534
4534
|
const res = await db.query({ [ns]: { $: q } });
|
|
4535
|
-
const
|
|
4536
|
-
return Array.isArray(
|
|
4535
|
+
const rows6 = res[ns];
|
|
4536
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
4537
4537
|
}
|
|
4538
4538
|
async function findApplication(db, event) {
|
|
4539
4539
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -4657,7 +4657,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4657
4657
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
4658
4658
|
const { eventId, event } = ingest;
|
|
4659
4659
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4660
|
-
const
|
|
4660
|
+
const digest2 = await stripeEventDigest(rawBody);
|
|
4661
4661
|
const identity = stripeEventIdentity(event);
|
|
4662
4662
|
const target = stripeEventTarget(env, ctx);
|
|
4663
4663
|
const preliminary = classifyMemberStripeEvent(identity, target, false);
|
|
@@ -4671,23 +4671,23 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4671
4671
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4672
4672
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4673
4673
|
}
|
|
4674
|
-
const replay2 = await replayedStripeReceipt(db, eventId,
|
|
4674
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest2, event);
|
|
4675
4675
|
if (replay2) return replay2;
|
|
4676
4676
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
4677
4677
|
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
4678
4678
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
4679
|
-
return quarantineStripeEvent(db, eventId,
|
|
4679
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4680
4680
|
}
|
|
4681
4681
|
if (event.kind === "gift_paid") {
|
|
4682
4682
|
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
4683
|
-
return quarantineStripeEvent(db, eventId,
|
|
4683
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4684
4684
|
}
|
|
4685
4685
|
try {
|
|
4686
4686
|
const issued = await issueGiftClaim(db, intent, {
|
|
4687
4687
|
eventId,
|
|
4688
4688
|
objectId: event.providerObjectId
|
|
4689
4689
|
}, Date.now());
|
|
4690
|
-
const stored3 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: true });
|
|
4690
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition: disposition2, matched: true });
|
|
4691
4691
|
if (stored3 === "collision") {
|
|
4692
4692
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4693
4693
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4703,7 +4703,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4703
4703
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
4704
4704
|
const claim = await firstRow3(db, "giftClaims", { where: { intentId: event.giftIntentId }, limit: 1 });
|
|
4705
4705
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
4706
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4706
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4707
4707
|
if (stored2 === "collision") {
|
|
4708
4708
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4709
4709
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4715,12 +4715,12 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4715
4715
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4716
4716
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4717
4717
|
}
|
|
4718
|
-
const replay = await replayedStripeReceipt(db, eventId,
|
|
4718
|
+
const replay = await replayedStripeReceipt(db, eventId, digest2, event);
|
|
4719
4719
|
if (replay) return replay;
|
|
4720
4720
|
const localApplication = Boolean(app && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
4721
4721
|
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
4722
4722
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
4723
|
-
return quarantineStripeEvent(db, eventId,
|
|
4723
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4724
4724
|
}
|
|
4725
4725
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
4726
4726
|
let next = app;
|
|
@@ -4743,7 +4743,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4743
4743
|
});
|
|
4744
4744
|
}
|
|
4745
4745
|
}
|
|
4746
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: true });
|
|
4746
|
+
const stored = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition, matched: true });
|
|
4747
4747
|
if (stored === "collision") {
|
|
4748
4748
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4749
4749
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4999,8 +4999,8 @@ async function upcomingEvents(env) {
|
|
|
4999
4999
|
const res = await cal.availability.upcoming();
|
|
5000
5000
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5001
5001
|
}
|
|
5002
|
-
function toReconcile(
|
|
5003
|
-
return
|
|
5002
|
+
function toReconcile(rows6) {
|
|
5003
|
+
return rows6.map((m) => ({
|
|
5004
5004
|
id: String(m.id),
|
|
5005
5005
|
applicationId: String(m.applicationId),
|
|
5006
5006
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5022,12 +5022,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5022
5022
|
const query = await db.query({
|
|
5023
5023
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5024
5024
|
});
|
|
5025
|
-
let
|
|
5026
|
-
if (Number.isFinite(from))
|
|
5027
|
-
if (Number.isFinite(to))
|
|
5025
|
+
let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5026
|
+
if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5027
|
+
if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5028
5028
|
let decisions = [];
|
|
5029
5029
|
try {
|
|
5030
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5030
|
+
decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
|
|
5031
5031
|
} catch {
|
|
5032
5032
|
}
|
|
5033
5033
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5054,7 +5054,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5054
5054
|
};
|
|
5055
5055
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5056
5056
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5057
|
-
const meetings =
|
|
5057
|
+
const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5058
5058
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5059
5059
|
};
|
|
5060
5060
|
|
|
@@ -5075,16 +5075,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
5075
5075
|
});
|
|
5076
5076
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
5077
5077
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
5078
|
-
const
|
|
5079
|
-
if (
|
|
5080
|
-
const result = await backfillCrm(crmDeps2(
|
|
5078
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5079
|
+
if (gate12 instanceof Response) return gate12;
|
|
5080
|
+
const result = await backfillCrm(crmDeps2(gate12.db, ctx));
|
|
5081
5081
|
return json({ ok: true, ...result });
|
|
5082
5082
|
};
|
|
5083
5083
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
5084
5084
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
5085
|
-
const
|
|
5086
|
-
if (
|
|
5087
|
-
const { db } =
|
|
5085
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5086
|
+
if (gate12 instanceof Response) return gate12;
|
|
5087
|
+
const { db } = gate12;
|
|
5088
5088
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
5089
5089
|
const [data, roleList] = await Promise.all([
|
|
5090
5090
|
db.query({
|
|
@@ -5130,16 +5130,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5130
5130
|
});
|
|
5131
5131
|
}
|
|
5132
5132
|
}
|
|
5133
|
-
const
|
|
5133
|
+
const rows6 = [...people.values()].sort(
|
|
5134
5134
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5135
5135
|
);
|
|
5136
|
-
return json({ people:
|
|
5136
|
+
return json({ people: rows6 });
|
|
5137
5137
|
};
|
|
5138
5138
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5139
5139
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
5140
|
-
const
|
|
5141
|
-
if (
|
|
5142
|
-
const { db } =
|
|
5140
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5141
|
+
if (gate12 instanceof Response) return gate12;
|
|
5142
|
+
const { db } = gate12;
|
|
5143
5143
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
5144
5144
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
5145
5145
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -5150,9 +5150,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
5150
5150
|
};
|
|
5151
5151
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
5152
5152
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
5153
|
-
const
|
|
5154
|
-
if (
|
|
5155
|
-
const { db, actor } =
|
|
5153
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5154
|
+
if (gate12 instanceof Response) return gate12;
|
|
5155
|
+
const { db, actor } = gate12;
|
|
5156
5156
|
let body;
|
|
5157
5157
|
try {
|
|
5158
5158
|
body = await req.json();
|
|
@@ -5188,8 +5188,8 @@ function canonical(value) {
|
|
|
5188
5188
|
}
|
|
5189
5189
|
return JSON.stringify(value) ?? "undefined";
|
|
5190
5190
|
}
|
|
5191
|
-
function positiveInt(
|
|
5192
|
-
const parsed = Number(
|
|
5191
|
+
function positiveInt(raw2, fallback, max) {
|
|
5192
|
+
const parsed = Number(raw2);
|
|
5193
5193
|
return Number.isSafeInteger(parsed) && parsed >= 0 ? Math.min(parsed, max) : fallback;
|
|
5194
5194
|
}
|
|
5195
5195
|
var handleAdminClerkPrivateProfileSync = async (req, url, env, ctx) => {
|
|
@@ -5270,14 +5270,14 @@ function periodSeries(points, now, count, periodMs) {
|
|
|
5270
5270
|
for (const point of points) {
|
|
5271
5271
|
if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
|
|
5272
5272
|
if (point.t >= start && point.t <= now) {
|
|
5273
|
-
const
|
|
5274
|
-
current[
|
|
5273
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
|
|
5274
|
+
current[index2] = (current[index2] ?? 0) + point.v;
|
|
5275
5275
|
} else if (point.t >= previousStart && point.t < start) {
|
|
5276
|
-
const
|
|
5277
|
-
previous[
|
|
5276
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
|
|
5277
|
+
previous[index2] = (previous[index2] ?? 0) + point.v;
|
|
5278
5278
|
}
|
|
5279
5279
|
}
|
|
5280
|
-
const labels = current.map((_,
|
|
5280
|
+
const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
|
|
5281
5281
|
return { labels, current, previous };
|
|
5282
5282
|
}
|
|
5283
5283
|
function dashboardMetricData(points, now) {
|
|
@@ -5690,11 +5690,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
5690
5690
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
5691
5691
|
const t = templates[key];
|
|
5692
5692
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
5693
|
-
const
|
|
5694
|
-
if (!subject || !
|
|
5693
|
+
const text5 = typeof t?.text === "string" ? t.text : "";
|
|
5694
|
+
if (!subject || !text5.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
5695
5695
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
5696
|
-
if (
|
|
5697
|
-
clean[key] = { subject, text:
|
|
5696
|
+
if (text5.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
5697
|
+
clean[key] = { subject, text: text5, enabled: t?.enabled !== false };
|
|
5698
5698
|
}
|
|
5699
5699
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5700
5700
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -6725,6 +6725,1192 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6725
6725
|
}
|
|
6726
6726
|
};
|
|
6727
6727
|
|
|
6728
|
+
// src/network-person-registry.ts
|
|
6729
|
+
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6730
|
+
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6731
|
+
function normalizeNetworkIdentity(kind, value) {
|
|
6732
|
+
const trimmed = value.trim();
|
|
6733
|
+
if (kind === "email") return trimmed.toLowerCase();
|
|
6734
|
+
if (kind === "phone") return trimmed.replace(/[\s().-]/g, "");
|
|
6735
|
+
return trimmed;
|
|
6736
|
+
}
|
|
6737
|
+
function emailLooksReal(value) {
|
|
6738
|
+
const at = value.lastIndexOf("@");
|
|
6739
|
+
if (at <= 0 || at === value.length - 1) return false;
|
|
6740
|
+
const domain = value.slice(at + 1).toLowerCase();
|
|
6741
|
+
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
6742
|
+
}
|
|
6743
|
+
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
6744
|
+
const errors = [];
|
|
6745
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, errors: ["import must be an object"] };
|
|
6746
|
+
const raw2 = input;
|
|
6747
|
+
if (raw2.sourceKind !== "external_reimport") errors.push("sourceKind must be external_reimport");
|
|
6748
|
+
if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
6749
|
+
const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
|
|
6750
|
+
const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
|
|
6751
|
+
const provenance = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
|
|
6752
|
+
const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
|
|
6753
|
+
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
6754
|
+
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6755
|
+
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6756
|
+
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6757
|
+
const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
|
|
6758
|
+
const consentStates = ["unknown", "granted", "denied"];
|
|
6759
|
+
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
6760
|
+
const discretionary = typeof rawConsent.discretionary === "string" ? rawConsent.discretionary : "";
|
|
6761
|
+
const recordedAt = Number(rawConsent.recordedAt);
|
|
6762
|
+
const basis = typeof rawConsent.basis === "string" ? rawConsent.basis.trim() : "";
|
|
6763
|
+
if (!consentStates.includes(marketing) || !consentStates.includes(discretionary) || !Number.isSafeInteger(recordedAt) || recordedAt <= 0 || !basis || basis.length > 500) {
|
|
6764
|
+
errors.push("consent must include marketing and discretionary states, recordedAt, and a bounded basis");
|
|
6765
|
+
}
|
|
6766
|
+
const identities = [];
|
|
6767
|
+
if (!Array.isArray(raw2.identities) || raw2.identities.length < 1 || raw2.identities.length > 8) {
|
|
6768
|
+
errors.push("one to eight verified identities are required");
|
|
6769
|
+
} else {
|
|
6770
|
+
for (const item of raw2.identities) {
|
|
6771
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6772
|
+
errors.push("identity must be an object");
|
|
6773
|
+
continue;
|
|
6774
|
+
}
|
|
6775
|
+
const candidate = item;
|
|
6776
|
+
const kind = candidate.kind;
|
|
6777
|
+
const value = typeof candidate.value === "string" ? candidate.value : "";
|
|
6778
|
+
const verifiedAt = Number(candidate.verifiedAt);
|
|
6779
|
+
if (kind !== "email" && kind !== "phone" && kind !== "external") {
|
|
6780
|
+
errors.push("identity kind is invalid");
|
|
6781
|
+
continue;
|
|
6782
|
+
}
|
|
6783
|
+
const normalized = normalizeNetworkIdentity(kind, value);
|
|
6784
|
+
if (!normalized || normalized.length > 320 || !Number.isSafeInteger(verifiedAt) || verifiedAt <= 0) {
|
|
6785
|
+
errors.push("identity value and verifiedAt are required and bounded");
|
|
6786
|
+
continue;
|
|
6787
|
+
}
|
|
6788
|
+
if (kind === "email" && !emailLooksReal(normalized)) {
|
|
6789
|
+
errors.push("development or invalid email identities cannot become Persons");
|
|
6790
|
+
continue;
|
|
6791
|
+
}
|
|
6792
|
+
identities.push({ kind, value: normalized, verifiedAt });
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6796
|
+
for (const identity of identities) {
|
|
6797
|
+
const key = `${identity.kind}:${identity.value}`;
|
|
6798
|
+
if (seen.has(key)) errors.push("duplicate identity in import");
|
|
6799
|
+
seen.add(key);
|
|
6800
|
+
}
|
|
6801
|
+
const chapterLinks = [];
|
|
6802
|
+
if (raw2.chapterLinks !== void 0) {
|
|
6803
|
+
if (!Array.isArray(raw2.chapterLinks) || raw2.chapterLinks.length > allowedChapterIds.length) errors.push("chapterLinks are invalid or too numerous");
|
|
6804
|
+
else for (const item of raw2.chapterLinks) {
|
|
6805
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6806
|
+
errors.push("chapter link must be an object");
|
|
6807
|
+
continue;
|
|
6808
|
+
}
|
|
6809
|
+
const chapterId = typeof item.chapterId === "string" ? String(item.chapterId) : "";
|
|
6810
|
+
const chapterPersonId = typeof item.chapterPersonId === "string" ? String(item.chapterPersonId).trim() : "";
|
|
6811
|
+
if (!CHAPTER_ID.test(chapterId) || !allowedChapterIds.includes(chapterId) || !chapterPersonId || chapterPersonId.length > 200) errors.push("chapter link is outside the configured network");
|
|
6812
|
+
else chapterLinks.push({ chapterId, chapterPersonId });
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
if (errors.length) return { ok: false, errors };
|
|
6816
|
+
return { ok: true, errors: [], value: {
|
|
6817
|
+
sourceKind: "external_reimport",
|
|
6818
|
+
sourceSystem,
|
|
6819
|
+
sourceRecordId,
|
|
6820
|
+
provenance,
|
|
6821
|
+
approved: true,
|
|
6822
|
+
isRealPerson: true,
|
|
6823
|
+
displayName,
|
|
6824
|
+
consent: {
|
|
6825
|
+
marketing,
|
|
6826
|
+
discretionary,
|
|
6827
|
+
recordedAt,
|
|
6828
|
+
basis
|
|
6829
|
+
},
|
|
6830
|
+
identities,
|
|
6831
|
+
...chapterLinks.length ? { chapterLinks } : {}
|
|
6832
|
+
} };
|
|
6833
|
+
}
|
|
6834
|
+
async function sha256(value) {
|
|
6835
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
6836
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
6837
|
+
}
|
|
6838
|
+
async function networkIdentityClaim(input) {
|
|
6839
|
+
const normalized = normalizeNetworkIdentity(input.kind, input.value);
|
|
6840
|
+
const digest2 = await sha256(`${input.kind}\0${normalized}`);
|
|
6841
|
+
const hint = input.kind === "email" ? `${normalized.slice(0, 1)}***@${normalized.split("@")[1]}` : input.kind === "phone" ? `***${normalized.slice(-4)}` : void 0;
|
|
6842
|
+
return { id: `identity_${digest2}`, kind: input.kind, value: normalized, ...hint ? { hint } : {}, verifiedAt: input.verifiedAt };
|
|
6843
|
+
}
|
|
6844
|
+
async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
6845
|
+
return `alias_${await sha256(`${sourceSystem}\0${sourceRecordId}`)}`;
|
|
6846
|
+
}
|
|
6847
|
+
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6848
|
+
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6849
|
+
}
|
|
6850
|
+
function projectNetworkContactPolicy(policy, links) {
|
|
6851
|
+
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6852
|
+
}
|
|
6853
|
+
function canonicalNetworkPersonId(personId, persons) {
|
|
6854
|
+
const byId = new Map(persons.map((person) => [person.id, person]));
|
|
6855
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6856
|
+
let current = personId;
|
|
6857
|
+
while (true) {
|
|
6858
|
+
if (seen.has(current)) return null;
|
|
6859
|
+
seen.add(current);
|
|
6860
|
+
const person = byId.get(current);
|
|
6861
|
+
if (!person) return null;
|
|
6862
|
+
if (!person.mergedIntoId) return person.id;
|
|
6863
|
+
current = person.mergedIntoId;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6867
|
+
// src/network-person-store.ts
|
|
6868
|
+
var NetworkPersonRegistryError = class extends Error {
|
|
6869
|
+
constructor(code, message) {
|
|
6870
|
+
super(message);
|
|
6871
|
+
this.code = code;
|
|
6872
|
+
}
|
|
6873
|
+
code;
|
|
6874
|
+
};
|
|
6875
|
+
var rows4 = (value) => Array.isArray(value) ? value : [];
|
|
6876
|
+
async function loadNetworkPersonRegistry(db) {
|
|
6877
|
+
const result = await db.query({
|
|
6878
|
+
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6879
|
+
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
6880
|
+
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6881
|
+
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
6882
|
+
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
6883
|
+
});
|
|
6884
|
+
return {
|
|
6885
|
+
persons: rows4(result.networkPersons),
|
|
6886
|
+
claims: rows4(result.networkIdentityClaims),
|
|
6887
|
+
aliases: rows4(result.networkPersonAliases),
|
|
6888
|
+
links: rows4(result.networkChapterPersonLinks),
|
|
6889
|
+
events: rows4(result.networkPersonEvents)
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
6893
|
+
t: "update",
|
|
6894
|
+
ns: "networkPersonEvents",
|
|
6895
|
+
id: `network_person_event:${mutationId}`,
|
|
6896
|
+
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
6897
|
+
});
|
|
6898
|
+
var defaultPolicy = (now, consent) => ({
|
|
6899
|
+
marketingSuppressed: consent.marketing !== "granted",
|
|
6900
|
+
discretionarySuppressed: consent.discretionary !== "granted",
|
|
6901
|
+
updatedAt: now
|
|
6902
|
+
});
|
|
6903
|
+
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
6904
|
+
async function linkOps(personId, links, now) {
|
|
6905
|
+
return Promise.all(links.map(async (link) => {
|
|
6906
|
+
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
6907
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, state: "shadow", createdAt: now } };
|
|
6908
|
+
}));
|
|
6909
|
+
}
|
|
6910
|
+
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
6911
|
+
const alias = {
|
|
6912
|
+
id: aliasId,
|
|
6913
|
+
sourceSystem: input.sourceSystem,
|
|
6914
|
+
sourceRecordId: input.sourceRecordId,
|
|
6915
|
+
sourceKey: aliasId,
|
|
6916
|
+
displayName: input.displayName,
|
|
6917
|
+
provenance: input.provenance,
|
|
6918
|
+
consent: input.consent,
|
|
6919
|
+
status: "ambiguous",
|
|
6920
|
+
candidatePersonIds: candidates,
|
|
6921
|
+
identityClaims: claims,
|
|
6922
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6923
|
+
revision: 1,
|
|
6924
|
+
createdAt: now,
|
|
6925
|
+
updatedAt: now
|
|
6926
|
+
};
|
|
6927
|
+
await db.transact([
|
|
6928
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
6929
|
+
eventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
6930
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
6931
|
+
return { status: "ambiguous", alias };
|
|
6932
|
+
}
|
|
6933
|
+
async function importNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
6934
|
+
const aliasId = await networkSourceKey(input.sourceSystem, input.sourceRecordId);
|
|
6935
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
6936
|
+
const replay = state.aliases.find((alias2) => alias2.id === aliasId);
|
|
6937
|
+
if (replay) return { status: replay.status, alias: replay, replayed: true };
|
|
6938
|
+
const claims = await Promise.all(input.identities.map(networkIdentityClaim));
|
|
6939
|
+
const candidates = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
6940
|
+
if (candidates.length) return ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now);
|
|
6941
|
+
const personId = `person_${aliasId.slice("alias_".length)}`;
|
|
6942
|
+
const person = { id: personId, displayName: input.displayName, state: "active", contactPolicy: defaultPolicy(now, input.consent), revision: 1, createdAt: now, updatedAt: now };
|
|
6943
|
+
const alias = {
|
|
6944
|
+
id: aliasId,
|
|
6945
|
+
sourceSystem: input.sourceSystem,
|
|
6946
|
+
sourceRecordId: input.sourceRecordId,
|
|
6947
|
+
sourceKey: aliasId,
|
|
6948
|
+
displayName: input.displayName,
|
|
6949
|
+
provenance: input.provenance,
|
|
6950
|
+
consent: input.consent,
|
|
6951
|
+
status: "linked",
|
|
6952
|
+
personId,
|
|
6953
|
+
identityClaims: claims,
|
|
6954
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6955
|
+
revision: 1,
|
|
6956
|
+
createdAt: now,
|
|
6957
|
+
updatedAt: now
|
|
6958
|
+
};
|
|
6959
|
+
const claimOps = claims.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId, createdAt: now } }));
|
|
6960
|
+
const ops = [
|
|
6961
|
+
{ t: "update", ns: "networkPersons", id: personId, attrs: { ...person } },
|
|
6962
|
+
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
6963
|
+
...claimOps,
|
|
6964
|
+
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
6965
|
+
eventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
6966
|
+
];
|
|
6967
|
+
try {
|
|
6968
|
+
await db.transact(ops, { mutationId, guards: [
|
|
6969
|
+
{ ns: "networkPersonAliases", id: aliasId, exists: false },
|
|
6970
|
+
...claims.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
6971
|
+
] });
|
|
6972
|
+
return { status: "linked", alias, person };
|
|
6973
|
+
} catch (error) {
|
|
6974
|
+
if (!isGuardConflict(error)) throw error;
|
|
6975
|
+
state = await loadNetworkPersonRegistry(db);
|
|
6976
|
+
const existing = state.aliases.find((item) => item.id === aliasId);
|
|
6977
|
+
if (existing) return { status: existing.status, alias: existing, replayed: true };
|
|
6978
|
+
const raced = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
6979
|
+
if (!raced.length) throw error;
|
|
6980
|
+
return ambiguousAlias(db, input, aliasId, claims, raced, `${mutationId}:review`, actor, now);
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date.now()) {
|
|
6984
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
6985
|
+
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
6986
|
+
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
6987
|
+
if (alias.revision !== input.expectedRevision || alias.status !== "ambiguous") throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
6988
|
+
if (input.decision === "exclude") {
|
|
6989
|
+
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
6990
|
+
await db.transact([
|
|
6991
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
6992
|
+
eventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
6993
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
6994
|
+
return next2;
|
|
6995
|
+
}
|
|
6996
|
+
const canonicalId = input.personId ? canonicalNetworkPersonId(input.personId, state.persons) : null;
|
|
6997
|
+
if (!canonicalId || canonicalId !== input.personId) throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
6998
|
+
const conflicting = alias.identityClaims.find((claim) => state.claims.some((row) => row.id === claim.id && row.personId !== canonicalId));
|
|
6999
|
+
if (conflicting) throw new NetworkPersonRegistryError("identity_conflict", "verified identity belongs to another Person; merge or choose that Person explicitly");
|
|
7000
|
+
const missing = alias.identityClaims.filter((claim) => !state.claims.some((row) => row.id === claim.id));
|
|
7001
|
+
const { candidatePersonIds: _candidatePersonIds, ...aliasWithoutCandidates } = alias;
|
|
7002
|
+
const next = { ...aliasWithoutCandidates, status: "linked", personId: canonicalId, revision: alias.revision + 1, updatedAt: now };
|
|
7003
|
+
await db.transact([
|
|
7004
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next } },
|
|
7005
|
+
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7006
|
+
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7007
|
+
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7008
|
+
eventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7009
|
+
], { mutationId, guards: [
|
|
7010
|
+
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7011
|
+
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
7012
|
+
] });
|
|
7013
|
+
return next;
|
|
7014
|
+
}
|
|
7015
|
+
async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now()) {
|
|
7016
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7017
|
+
const source = state.persons.find((person) => person.id === input.sourcePersonId);
|
|
7018
|
+
const target = state.persons.find((person) => person.id === input.targetPersonId);
|
|
7019
|
+
if (!source || !target || source.id === target.id || source.state !== "active" || target.state !== "active") throw new NetworkPersonRegistryError("invalid_merge", "merge requires two distinct active Persons");
|
|
7020
|
+
if (source.revision !== input.expectedRevision || target.revision !== input.expectedTargetRevision || canonicalNetworkPersonId(target.id, state.persons) !== target.id) {
|
|
7021
|
+
throw new NetworkPersonRegistryError("stale_merge", "Person changed; reload before merging");
|
|
7022
|
+
}
|
|
7023
|
+
const sourceNext = { ...source, state: "merged", mergedIntoId: target.id, revision: source.revision + 1, updatedAt: now };
|
|
7024
|
+
const targetNext = {
|
|
7025
|
+
...target,
|
|
7026
|
+
contactPolicy: {
|
|
7027
|
+
marketingSuppressed: source.contactPolicy.marketingSuppressed || target.contactPolicy.marketingSuppressed,
|
|
7028
|
+
discretionarySuppressed: source.contactPolicy.discretionarySuppressed || target.contactPolicy.discretionarySuppressed,
|
|
7029
|
+
...target.contactPolicy.reason || source.contactPolicy.reason ? { reason: target.contactPolicy.reason ?? source.contactPolicy.reason } : {},
|
|
7030
|
+
updatedAt: now
|
|
7031
|
+
},
|
|
7032
|
+
revision: target.revision + 1,
|
|
7033
|
+
updatedAt: now
|
|
7034
|
+
};
|
|
7035
|
+
await db.transact([
|
|
7036
|
+
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7037
|
+
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7038
|
+
eventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7039
|
+
], { mutationId, guards: [
|
|
7040
|
+
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7041
|
+
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
7042
|
+
] });
|
|
7043
|
+
return { source: sourceNext, target: targetNext };
|
|
7044
|
+
}
|
|
7045
|
+
async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7046
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7047
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7048
|
+
if (!person || person.state !== "merged" || person.revision !== input.expectedRevision) throw new NetworkPersonRegistryError("stale_unmerge", "merged Person changed; reload before unmerging");
|
|
7049
|
+
const next = { ...person, state: "active", revision: person.revision + 1, updatedAt: now };
|
|
7050
|
+
delete next.mergedIntoId;
|
|
7051
|
+
await db.transact([
|
|
7052
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7053
|
+
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7054
|
+
eventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7055
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7056
|
+
return next;
|
|
7057
|
+
}
|
|
7058
|
+
async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7059
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7060
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7061
|
+
if (!person || person.state !== "active" || canonicalNetworkPersonId(person.id, state.persons) !== person.id || person.revision !== input.expectedRevision) {
|
|
7062
|
+
throw new NetworkPersonRegistryError("stale_suppression", "active canonical Person changed; reload before updating suppression");
|
|
7063
|
+
}
|
|
7064
|
+
const policy = { marketingSuppressed: input.marketingSuppressed, discretionarySuppressed: input.discretionarySuppressed, ...input.reason ? { reason: input.reason } : {}, updatedAt: now };
|
|
7065
|
+
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7066
|
+
await db.transact([
|
|
7067
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7068
|
+
eventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7069
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7070
|
+
return next;
|
|
7071
|
+
}
|
|
7072
|
+
|
|
7073
|
+
// src/network-person-reveal.ts
|
|
7074
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7075
|
+
const reason = input.reason.trim();
|
|
7076
|
+
if (!reason || reason.length > 500) {
|
|
7077
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7078
|
+
}
|
|
7079
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7080
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7081
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7082
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7083
|
+
const event = {
|
|
7084
|
+
t: "update",
|
|
7085
|
+
ns: "networkPersonEvents",
|
|
7086
|
+
id: `network_person_event:${mutationId}`,
|
|
7087
|
+
attrs: {
|
|
7088
|
+
id: `network_person_event:${mutationId}`,
|
|
7089
|
+
kind: "pii_revealed",
|
|
7090
|
+
actorId: actor.id,
|
|
7091
|
+
...ids,
|
|
7092
|
+
mutationId,
|
|
7093
|
+
detail: { targetKind: input.kind, reason },
|
|
7094
|
+
createdAt: now
|
|
7095
|
+
}
|
|
7096
|
+
};
|
|
7097
|
+
await db.transact([event], {
|
|
7098
|
+
mutationId,
|
|
7099
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7100
|
+
});
|
|
7101
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7102
|
+
}
|
|
7103
|
+
|
|
7104
|
+
// src/worker-routes-network-person-registry.ts
|
|
7105
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7106
|
+
var MAX_BODY2 = 32e3;
|
|
7107
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7108
|
+
function redactedName(displayName) {
|
|
7109
|
+
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7110
|
+
}
|
|
7111
|
+
var redactedLink = (link) => ({
|
|
7112
|
+
id: link.id,
|
|
7113
|
+
personId: link.personId,
|
|
7114
|
+
chapterId: link.chapterId,
|
|
7115
|
+
state: link.state,
|
|
7116
|
+
createdAt: link.createdAt
|
|
7117
|
+
});
|
|
7118
|
+
var safePolicy = (policy) => ({
|
|
7119
|
+
marketingSuppressed: policy.marketingSuppressed,
|
|
7120
|
+
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7121
|
+
hasReason: Boolean(policy.reason),
|
|
7122
|
+
updatedAt: policy.updatedAt
|
|
7123
|
+
});
|
|
7124
|
+
var safeAlias = (alias) => ({
|
|
7125
|
+
id: alias.id,
|
|
7126
|
+
sourceSystem: alias.sourceSystem,
|
|
7127
|
+
displayNameHint: redactedName(alias.displayName),
|
|
7128
|
+
status: alias.status,
|
|
7129
|
+
personId: alias.personId ?? null,
|
|
7130
|
+
candidatePersonIds: alias.candidatePersonIds ?? [],
|
|
7131
|
+
consent: {
|
|
7132
|
+
marketing: alias.consent.marketing,
|
|
7133
|
+
discretionary: alias.consent.discretionary,
|
|
7134
|
+
recordedAt: alias.consent.recordedAt,
|
|
7135
|
+
hasBasis: Boolean(alias.consent.basis)
|
|
7136
|
+
},
|
|
7137
|
+
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7138
|
+
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7139
|
+
revision: alias.revision,
|
|
7140
|
+
updatedAt: alias.updatedAt
|
|
7141
|
+
});
|
|
7142
|
+
var safePerson = (person) => ({
|
|
7143
|
+
id: person.id,
|
|
7144
|
+
displayNameHint: redactedName(person.displayName),
|
|
7145
|
+
state: person.state,
|
|
7146
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7147
|
+
revision: person.revision,
|
|
7148
|
+
contactPolicy: safePolicy(person.contactPolicy)
|
|
7149
|
+
});
|
|
7150
|
+
var positiveRevision = (value) => {
|
|
7151
|
+
const parsed = Number(value);
|
|
7152
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
7153
|
+
};
|
|
7154
|
+
async function gate10(req, env, ctx) {
|
|
7155
|
+
const db = ctx.makeDb(env);
|
|
7156
|
+
const user = await ctx.verifyUser(req, env);
|
|
7157
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7158
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7159
|
+
return { db, user };
|
|
7160
|
+
}
|
|
7161
|
+
function registryError(error) {
|
|
7162
|
+
if (error instanceof NetworkPersonRegistryError) {
|
|
7163
|
+
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7164
|
+
return json({ error: error.message, code: error.code }, status);
|
|
7165
|
+
}
|
|
7166
|
+
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
7167
|
+
return json({ error: "registry changed; reload and retry", code: "stale_write" }, 409);
|
|
7168
|
+
}
|
|
7169
|
+
return null;
|
|
7170
|
+
}
|
|
7171
|
+
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
7172
|
+
if (url.pathname !== PATH2) return null;
|
|
7173
|
+
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
7174
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7175
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
7176
|
+
return json({ error: "request is too large" }, 413);
|
|
7177
|
+
}
|
|
7178
|
+
const got = await gate10(req, env, ctx);
|
|
7179
|
+
if ("response" in got) return got.response;
|
|
7180
|
+
const db = got.db;
|
|
7181
|
+
if (req.method === "POST") {
|
|
7182
|
+
let body;
|
|
7183
|
+
try {
|
|
7184
|
+
const text5 = await req.text();
|
|
7185
|
+
if (new TextEncoder().encode(text5).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7186
|
+
const parsed = JSON.parse(text5);
|
|
7187
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7188
|
+
body = parsed;
|
|
7189
|
+
} catch {
|
|
7190
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7191
|
+
}
|
|
7192
|
+
const command = typeof body.command === "string" ? body.command : "";
|
|
7193
|
+
const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
|
|
7194
|
+
const input = body.input && typeof body.input === "object" && !Array.isArray(body.input) ? body.input : {};
|
|
7195
|
+
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7196
|
+
const actionActor = { id: got.user.userId };
|
|
7197
|
+
try {
|
|
7198
|
+
if (command === "import") {
|
|
7199
|
+
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7200
|
+
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|
|
7201
|
+
const result = await importNetworkPerson(db, validated.value, mutationId, actionActor);
|
|
7202
|
+
return json({
|
|
7203
|
+
result: {
|
|
7204
|
+
status: result.status,
|
|
7205
|
+
alias: safeAlias(result.alias),
|
|
7206
|
+
..."person" in result && result.person ? { person: safePerson(result.person) } : {},
|
|
7207
|
+
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7208
|
+
}
|
|
7209
|
+
}, 201);
|
|
7210
|
+
}
|
|
7211
|
+
if (command === "review") {
|
|
7212
|
+
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or exclude" }, 400);
|
|
7213
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7214
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7215
|
+
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
7216
|
+
const result = await reviewNetworkPersonAlias(db, {
|
|
7217
|
+
aliasId: typeof input.aliasId === "string" ? input.aliasId : "",
|
|
7218
|
+
expectedRevision,
|
|
7219
|
+
decision: input.decision,
|
|
7220
|
+
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7221
|
+
}, mutationId, actionActor);
|
|
7222
|
+
return json({ result: safeAlias(result) });
|
|
7223
|
+
}
|
|
7224
|
+
if (command === "merge") {
|
|
7225
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7226
|
+
const expectedTargetRevision = positiveRevision(input.expectedTargetRevision);
|
|
7227
|
+
if (!expectedRevision || !expectedTargetRevision) return json({ error: "source and target revisions are required" }, 400);
|
|
7228
|
+
const result = await mergeNetworkPersons(db, {
|
|
7229
|
+
sourcePersonId: typeof input.sourcePersonId === "string" ? input.sourcePersonId : "",
|
|
7230
|
+
targetPersonId: typeof input.targetPersonId === "string" ? input.targetPersonId : "",
|
|
7231
|
+
expectedRevision,
|
|
7232
|
+
expectedTargetRevision
|
|
7233
|
+
}, mutationId, actionActor);
|
|
7234
|
+
return json({ result: { source: safePerson(result.source), target: safePerson(result.target) } });
|
|
7235
|
+
}
|
|
7236
|
+
if (command === "unmerge") {
|
|
7237
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7238
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7239
|
+
const result = await unmergeNetworkPerson(db, {
|
|
7240
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7241
|
+
expectedRevision
|
|
7242
|
+
}, mutationId, actionActor);
|
|
7243
|
+
return json({ result: safePerson(result) });
|
|
7244
|
+
}
|
|
7245
|
+
if (command === "suppression") {
|
|
7246
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7247
|
+
if (!expectedRevision || typeof input.marketingSuppressed !== "boolean" || typeof input.discretionarySuppressed !== "boolean") {
|
|
7248
|
+
return json({ error: "expectedRevision and both suppression flags are required" }, 400);
|
|
7249
|
+
}
|
|
7250
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7251
|
+
if (reason.length > 500) return json({ error: "suppression reason is too long" }, 400);
|
|
7252
|
+
const result = await suppressNetworkPerson(db, {
|
|
7253
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7254
|
+
expectedRevision,
|
|
7255
|
+
marketingSuppressed: input.marketingSuppressed,
|
|
7256
|
+
discretionarySuppressed: input.discretionarySuppressed,
|
|
7257
|
+
...reason ? { reason } : {}
|
|
7258
|
+
}, mutationId, actionActor);
|
|
7259
|
+
return json({ result: safePerson(result) });
|
|
7260
|
+
}
|
|
7261
|
+
if (command === "reveal") {
|
|
7262
|
+
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
7263
|
+
const result = await revealNetworkPersonRegistryRecord(db, {
|
|
7264
|
+
kind: input.kind,
|
|
7265
|
+
id: typeof input.id === "string" ? input.id : "",
|
|
7266
|
+
reason: typeof input.reason === "string" ? input.reason : ""
|
|
7267
|
+
}, mutationId, actionActor);
|
|
7268
|
+
return json({ sensitive: true, result });
|
|
7269
|
+
}
|
|
7270
|
+
return json({ error: "unknown registry command" }, 400);
|
|
7271
|
+
} catch (error) {
|
|
7272
|
+
const response = registryError(error);
|
|
7273
|
+
if (response) return response;
|
|
7274
|
+
throw error;
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7278
|
+
const persons = state.persons.map((person) => {
|
|
7279
|
+
const links = state.links.filter((link) => link.personId === person.id);
|
|
7280
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7281
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7282
|
+
return {
|
|
7283
|
+
id: person.id,
|
|
7284
|
+
displayNameHint: redactedName(person.displayName),
|
|
7285
|
+
state: person.state,
|
|
7286
|
+
canonicalPersonId: canonicalId,
|
|
7287
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7288
|
+
revision: person.revision,
|
|
7289
|
+
contactPolicy: safePolicy(person.contactPolicy),
|
|
7290
|
+
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7291
|
+
chapterLinks: links.map(redactedLink),
|
|
7292
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, links.map(redactedLink)).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7293
|
+
};
|
|
7294
|
+
});
|
|
7295
|
+
const aliases = state.aliases.map(safeAlias);
|
|
7296
|
+
return json({
|
|
7297
|
+
version: 1,
|
|
7298
|
+
mode: "shadow",
|
|
7299
|
+
writable: true,
|
|
7300
|
+
authoritativeForChapterOperation: false,
|
|
7301
|
+
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7302
|
+
persons,
|
|
7303
|
+
aliases,
|
|
7304
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7305
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7306
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7307
|
+
id: event.id,
|
|
7308
|
+
kind: event.kind,
|
|
7309
|
+
personId: event.personId ?? null,
|
|
7310
|
+
aliasId: event.aliasId ?? null,
|
|
7311
|
+
actorId: event.actorId,
|
|
7312
|
+
mutationId: event.mutationId,
|
|
7313
|
+
createdAt: event.createdAt
|
|
7314
|
+
}))
|
|
7315
|
+
});
|
|
7316
|
+
};
|
|
7317
|
+
|
|
7318
|
+
// src/worker-routes-network-commercial.ts
|
|
7319
|
+
import { constructStripeEvent, initStripe, StripeError } from "@odla-ai/stripe";
|
|
7320
|
+
|
|
7321
|
+
// src/config-network-commercial.ts
|
|
7322
|
+
function networkCommercialSourceKey(source) {
|
|
7323
|
+
return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
|
|
7324
|
+
}
|
|
7325
|
+
|
|
7326
|
+
// src/network-commercial.ts
|
|
7327
|
+
var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7328
|
+
var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
|
|
7329
|
+
var integer2 = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
7330
|
+
var bool = (value) => typeof value === "boolean" ? value : void 0;
|
|
7331
|
+
var providerId = (value, prefix) => {
|
|
7332
|
+
const valueText = text3(value);
|
|
7333
|
+
return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
|
|
7334
|
+
};
|
|
7335
|
+
function metadataCandidates(object) {
|
|
7336
|
+
const parent = raw(object.parent);
|
|
7337
|
+
const subscriptionDetails = raw(object.subscription_details);
|
|
7338
|
+
const parentSubscription = raw(parent.subscription_details);
|
|
7339
|
+
return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
|
|
7340
|
+
}
|
|
7341
|
+
function metadataValue(candidates, key) {
|
|
7342
|
+
return candidates.map((candidate) => text3(candidate[key])).find(Boolean);
|
|
7343
|
+
}
|
|
7344
|
+
function refId(value, prefix) {
|
|
7345
|
+
if (typeof value === "string") return providerId(value, prefix);
|
|
7346
|
+
return providerId(raw(value).id, prefix);
|
|
7347
|
+
}
|
|
7348
|
+
function subscriptionRef(object) {
|
|
7349
|
+
return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
|
|
7350
|
+
}
|
|
7351
|
+
function firstPrice(object) {
|
|
7352
|
+
const data = raw(object.items).data;
|
|
7353
|
+
const item = Array.isArray(data) ? raw(data[0]) : {};
|
|
7354
|
+
const priceId = refId(item.price, "price_");
|
|
7355
|
+
const quantity = integer2(item.quantity);
|
|
7356
|
+
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
7357
|
+
}
|
|
7358
|
+
function normalizeNetworkCommercialStripeEvent(input, digest2) {
|
|
7359
|
+
const event = raw(input);
|
|
7360
|
+
const eventId = providerId(event.id, "evt_") ?? "";
|
|
7361
|
+
const eventType = text3(event.type) ?? "";
|
|
7362
|
+
const object = raw(raw(event.data).object);
|
|
7363
|
+
const objectId = providerId(object.id) ?? "";
|
|
7364
|
+
const candidates = metadataCandidates(object);
|
|
7365
|
+
const identity = {};
|
|
7366
|
+
const identityKeys = {
|
|
7367
|
+
appId: "odlaAppId",
|
|
7368
|
+
chapterId: "chapterId",
|
|
7369
|
+
environment: "odlaEnvironment",
|
|
7370
|
+
dataEnvironment: "odlaDataEnvironment",
|
|
7371
|
+
runtime: "odlaRuntime"
|
|
7372
|
+
};
|
|
7373
|
+
for (const [target, source] of Object.entries(identityKeys)) {
|
|
7374
|
+
const value = metadataValue(candidates, source);
|
|
7375
|
+
if (value) identity[target] = value;
|
|
7376
|
+
}
|
|
7377
|
+
let objectKind = "ignored";
|
|
7378
|
+
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
7379
|
+
else if (eventType.startsWith("customer.")) objectKind = "customer";
|
|
7380
|
+
else if (eventType.startsWith("invoice.")) objectKind = "invoice";
|
|
7381
|
+
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
7382
|
+
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
7383
|
+
const customerId = refId(object.customer, "cus_");
|
|
7384
|
+
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
|
|
7385
|
+
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
|
|
7386
|
+
const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
|
|
7387
|
+
const paymentIntentId = refId(object.payment_intent, "pi_");
|
|
7388
|
+
const price = firstPrice(object);
|
|
7389
|
+
const amountCents = integer2(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
|
|
7390
|
+
const currency = text3(object.currency)?.toLowerCase();
|
|
7391
|
+
const status = text3(object.status);
|
|
7392
|
+
return {
|
|
7393
|
+
eventId,
|
|
7394
|
+
digest: digest2,
|
|
7395
|
+
eventType,
|
|
7396
|
+
eventCreated: integer2(event.created) ?? 0,
|
|
7397
|
+
livemode: event.livemode === true,
|
|
7398
|
+
objectKind,
|
|
7399
|
+
objectId,
|
|
7400
|
+
identity,
|
|
7401
|
+
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
7402
|
+
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
7403
|
+
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
7404
|
+
...customerId ? { customerId } : {},
|
|
7405
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
7406
|
+
...invoiceId ? { invoiceId } : {},
|
|
7407
|
+
...chargeId ? { chargeId } : {},
|
|
7408
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
7409
|
+
...price,
|
|
7410
|
+
...status ? { status } : {},
|
|
7411
|
+
...currency ? { currency } : {},
|
|
7412
|
+
...amountCents !== void 0 ? { amountCents } : {},
|
|
7413
|
+
...integer2(object.amount_paid) !== void 0 ? { amountPaidCents: integer2(object.amount_paid) } : {},
|
|
7414
|
+
...integer2(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer2(object.amount_remaining) } : {},
|
|
7415
|
+
...integer2(object.current_period_start) !== void 0 ? { currentPeriodStart: integer2(object.current_period_start) } : {},
|
|
7416
|
+
...integer2(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer2(object.current_period_end) } : {},
|
|
7417
|
+
...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
|
|
7418
|
+
};
|
|
7419
|
+
}
|
|
7420
|
+
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
7421
|
+
function directSource(fact, sources) {
|
|
7422
|
+
const mode = fact.livemode ? "live" : "test";
|
|
7423
|
+
const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
|
|
7424
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
|
|
7425
|
+
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
7426
|
+
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
7427
|
+
return null;
|
|
7428
|
+
}
|
|
7429
|
+
var objectRefs = (fact) => [
|
|
7430
|
+
fact.customerId && `customer:${fact.customerId}`,
|
|
7431
|
+
fact.subscriptionId && `subscription:${fact.subscriptionId}`,
|
|
7432
|
+
fact.invoiceId && `invoice:${fact.invoiceId}`,
|
|
7433
|
+
fact.chargeId && `charge:${fact.chargeId}`,
|
|
7434
|
+
fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
|
|
7435
|
+
].filter((value) => Boolean(value));
|
|
7436
|
+
function ownRef(fact) {
|
|
7437
|
+
if (!fact.objectId) return null;
|
|
7438
|
+
if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
|
|
7439
|
+
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
7440
|
+
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
7441
|
+
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
7442
|
+
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
7443
|
+
}
|
|
7444
|
+
var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
|
|
7445
|
+
function projectNetworkCommercialEvents(input, sources) {
|
|
7446
|
+
const facts2 = [...input].sort(order);
|
|
7447
|
+
const exceptions = [];
|
|
7448
|
+
const unique = [];
|
|
7449
|
+
const digests = /* @__PURE__ */ new Map();
|
|
7450
|
+
for (const fact of facts2) {
|
|
7451
|
+
const prior = digests.get(fact.eventId);
|
|
7452
|
+
if (prior === void 0) {
|
|
7453
|
+
digests.set(fact.eventId, fact.digest);
|
|
7454
|
+
unique.push(fact);
|
|
7455
|
+
} else if (prior !== fact.digest) exceptions.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7456
|
+
}
|
|
7457
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
7458
|
+
const refs = /* @__PURE__ */ new Map();
|
|
7459
|
+
for (const fact of unique) {
|
|
7460
|
+
const direct = directSource(fact, sources);
|
|
7461
|
+
if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
|
|
7462
|
+
}
|
|
7463
|
+
let changed = true;
|
|
7464
|
+
while (changed) {
|
|
7465
|
+
changed = false;
|
|
7466
|
+
for (const fact of unique) {
|
|
7467
|
+
const assigned = assignments.get(fact.eventId);
|
|
7468
|
+
if (assigned) {
|
|
7469
|
+
const key = networkCommercialSourceKey(assigned);
|
|
7470
|
+
for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
|
|
7471
|
+
const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
|
|
7472
|
+
const before = keys2.size;
|
|
7473
|
+
keys2.add(key);
|
|
7474
|
+
refs.set(ref, keys2);
|
|
7475
|
+
if (keys2.size !== before) changed = true;
|
|
7476
|
+
}
|
|
7477
|
+
continue;
|
|
7478
|
+
}
|
|
7479
|
+
if (directSource(fact, sources) === "wrong") continue;
|
|
7480
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
7481
|
+
if (keys.size !== 1) continue;
|
|
7482
|
+
const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
|
|
7483
|
+
if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
|
|
7484
|
+
assignments.set(fact.eventId, candidate);
|
|
7485
|
+
changed = true;
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
const latest = /* @__PURE__ */ new Map();
|
|
7490
|
+
for (const fact of unique) {
|
|
7491
|
+
if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
|
|
7492
|
+
exceptions.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7493
|
+
continue;
|
|
7494
|
+
}
|
|
7495
|
+
const source = assignments.get(fact.eventId);
|
|
7496
|
+
if (!source) {
|
|
7497
|
+
const direct = directSource(fact, sources);
|
|
7498
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
7499
|
+
exceptions.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7500
|
+
continue;
|
|
7501
|
+
}
|
|
7502
|
+
const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
|
|
7503
|
+
const key = `${fact.objectKind}:${fact.objectId}`;
|
|
7504
|
+
const prior = latest.get(key);
|
|
7505
|
+
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7506
|
+
}
|
|
7507
|
+
const rows6 = [...latest.values()];
|
|
7508
|
+
const customers = rows6.filter((row) => row.objectKind === "customer");
|
|
7509
|
+
const subscriptions = rows6.filter((row) => row.objectKind === "subscription");
|
|
7510
|
+
const invoices = rows6.filter((row) => row.objectKind === "invoice");
|
|
7511
|
+
const refunds = rows6.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
7512
|
+
for (const charge of rows6.filter((row) => row.eventType === "charge.refunded")) {
|
|
7513
|
+
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7514
|
+
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
const totalsMap = /* @__PURE__ */ new Map();
|
|
7518
|
+
const add = (row, field, amount) => {
|
|
7519
|
+
if (!row.currency) return;
|
|
7520
|
+
const key = `${row.sourceKey}:${row.currency}`;
|
|
7521
|
+
const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
7522
|
+
total[field] += amount;
|
|
7523
|
+
total.netCents = total.paidCents - total.refundedCents;
|
|
7524
|
+
totalsMap.set(key, total);
|
|
7525
|
+
};
|
|
7526
|
+
for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
|
|
7527
|
+
for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
|
|
7528
|
+
return { customers, subscriptions, invoices, refunds, exceptions, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
|
|
7529
|
+
}
|
|
7530
|
+
|
|
7531
|
+
// src/network-commercial-store.ts
|
|
7532
|
+
var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
|
|
7533
|
+
var projectionId = (kind, sourceKey, providerId2) => `network-commercial:${kind}:${sourceKey}:${providerId2}`;
|
|
7534
|
+
var exceptionId = (eventId, kind) => `network-commercial-exception:${eventId}:${kind}`;
|
|
7535
|
+
var materializedNamespaces = [
|
|
7536
|
+
"networkCommercialCustomers",
|
|
7537
|
+
"networkCommercialSubscriptions",
|
|
7538
|
+
"networkCommercialInvoices",
|
|
7539
|
+
"networkCommercialRefunds"
|
|
7540
|
+
];
|
|
7541
|
+
async function rows5(db, ns) {
|
|
7542
|
+
const all = [];
|
|
7543
|
+
const pageSize = 500;
|
|
7544
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
7545
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset } } });
|
|
7546
|
+
const page = Array.isArray(result[ns]) ? result[ns] : [];
|
|
7547
|
+
all.push(...page);
|
|
7548
|
+
if (page.length < pageSize) return all;
|
|
7549
|
+
}
|
|
7550
|
+
}
|
|
7551
|
+
async function loadNetworkCommercialProjection(db, sources) {
|
|
7552
|
+
const stored = await rows5(db, "networkCommercialEvents");
|
|
7553
|
+
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7554
|
+
const persisted = await rows5(db, "networkCommercialExceptions");
|
|
7555
|
+
for (const row of persisted) {
|
|
7556
|
+
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7557
|
+
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
7558
|
+
projection.exceptions.push({
|
|
7559
|
+
eventId: String(row.eventId),
|
|
7560
|
+
kind: "collision",
|
|
7561
|
+
...typeof row.sourceKey === "string" ? { sourceKey: row.sourceKey } : {},
|
|
7562
|
+
detail: row.detail
|
|
7563
|
+
});
|
|
7564
|
+
}
|
|
7565
|
+
return projection;
|
|
7566
|
+
}
|
|
7567
|
+
function projectionOps(projection, now) {
|
|
7568
|
+
const groups = [
|
|
7569
|
+
["networkCommercialCustomers", "customer", projection.customers],
|
|
7570
|
+
["networkCommercialSubscriptions", "subscription", projection.subscriptions],
|
|
7571
|
+
["networkCommercialInvoices", "invoice", projection.invoices],
|
|
7572
|
+
["networkCommercialRefunds", "refund", projection.refunds]
|
|
7573
|
+
];
|
|
7574
|
+
return groups.flatMap(([ns, kind, projected]) => projected.map((snapshot) => ({
|
|
7575
|
+
t: "update",
|
|
7576
|
+
ns,
|
|
7577
|
+
id: projectionId(kind, snapshot.sourceKey, snapshot.objectId),
|
|
7578
|
+
attrs: {
|
|
7579
|
+
id: projectionId(kind, snapshot.sourceKey, snapshot.objectId),
|
|
7580
|
+
providerId: snapshot.objectId,
|
|
7581
|
+
sourceKey: snapshot.sourceKey,
|
|
7582
|
+
status: "active",
|
|
7583
|
+
snapshot,
|
|
7584
|
+
eventCreated: snapshot.eventCreated,
|
|
7585
|
+
updatedAt: now
|
|
7586
|
+
}
|
|
7587
|
+
})));
|
|
7588
|
+
}
|
|
7589
|
+
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7590
|
+
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7591
|
+
const [existing, ...materialized] = await Promise.all([
|
|
7592
|
+
rows5(db, "networkCommercialExceptions"),
|
|
7593
|
+
...materializedNamespaces.map((ns) => rows5(db, ns))
|
|
7594
|
+
]);
|
|
7595
|
+
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7596
|
+
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
7597
|
+
const ops = [
|
|
7598
|
+
...projectionOps(projection, now),
|
|
7599
|
+
...materializedNamespaces.flatMap((ns, index2) => materialized[index2].filter((row) => row.status === "active" && !projectedIds.has(String(row.id))).map((row) => ({ t: "update", ns, id: String(row.id), attrs: { status: "inactive", updatedAt: now } }))),
|
|
7600
|
+
...existing.filter((row) => row.status === "active" && !activeIds.has(String(row.id))).map((row) => ({
|
|
7601
|
+
t: "update",
|
|
7602
|
+
ns: "networkCommercialExceptions",
|
|
7603
|
+
id: String(row.id),
|
|
7604
|
+
attrs: { status: "resolved", updatedAt: now }
|
|
7605
|
+
})),
|
|
7606
|
+
...projection.exceptions.map((item) => ({
|
|
7607
|
+
t: "update",
|
|
7608
|
+
ns: "networkCommercialExceptions",
|
|
7609
|
+
id: exceptionId(item.eventId, item.kind),
|
|
7610
|
+
attrs: {
|
|
7611
|
+
id: exceptionId(item.eventId, item.kind),
|
|
7612
|
+
eventId: item.eventId,
|
|
7613
|
+
kind: item.kind,
|
|
7614
|
+
...item.sourceKey ? { sourceKey: item.sourceKey } : {},
|
|
7615
|
+
status: "active",
|
|
7616
|
+
detail: item.detail,
|
|
7617
|
+
updatedAt: now
|
|
7618
|
+
}
|
|
7619
|
+
}))
|
|
7620
|
+
];
|
|
7621
|
+
if (ops.length > 0) await db.transact(ops, { mutationId });
|
|
7622
|
+
return projection;
|
|
7623
|
+
}
|
|
7624
|
+
async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now()) {
|
|
7625
|
+
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
7626
|
+
const existing = found.networkCommercialEvents?.[0];
|
|
7627
|
+
if (existing) {
|
|
7628
|
+
const projection2 = await loadNetworkCommercialProjection(db, sources);
|
|
7629
|
+
if (existing.digest === fact.digest) return { status: "replayed", projection: projection2 };
|
|
7630
|
+
const id2 = exceptionId(fact.eventId, "collision");
|
|
7631
|
+
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id2, attrs: {
|
|
7632
|
+
id: id2,
|
|
7633
|
+
eventId: fact.eventId,
|
|
7634
|
+
kind: "collision",
|
|
7635
|
+
status: "active",
|
|
7636
|
+
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
7637
|
+
updatedAt: now
|
|
7638
|
+
} }], { mutationId: `network-commercial-collision:${fact.eventId}:${fact.digest}` });
|
|
7639
|
+
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
7640
|
+
}
|
|
7641
|
+
const id = eventRowId(fact.eventId);
|
|
7642
|
+
let lostRace = false;
|
|
7643
|
+
let duplicate = false;
|
|
7644
|
+
try {
|
|
7645
|
+
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id, attrs: {
|
|
7646
|
+
id,
|
|
7647
|
+
eventId: fact.eventId,
|
|
7648
|
+
digest: fact.digest,
|
|
7649
|
+
eventType: fact.eventType,
|
|
7650
|
+
eventCreated: fact.eventCreated,
|
|
7651
|
+
livemode: fact.livemode,
|
|
7652
|
+
fact,
|
|
7653
|
+
receivedAt: now
|
|
7654
|
+
} }], {
|
|
7655
|
+
mutationId: `network-commercial-ingest:${fact.eventId}:${fact.digest}`,
|
|
7656
|
+
guards: [{ ns: "networkCommercialEvents", id, exists: false }]
|
|
7657
|
+
});
|
|
7658
|
+
duplicate = write.duplicate;
|
|
7659
|
+
} catch (error) {
|
|
7660
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
7661
|
+
lostRace = true;
|
|
7662
|
+
}
|
|
7663
|
+
if (duplicate || lostRace) {
|
|
7664
|
+
const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
7665
|
+
const row = raced.networkCommercialEvents?.[0];
|
|
7666
|
+
const projection2 = await loadNetworkCommercialProjection(db, sources);
|
|
7667
|
+
if (row?.digest === fact.digest) return { status: "replayed", projection: projection2 };
|
|
7668
|
+
const id2 = exceptionId(fact.eventId, "collision");
|
|
7669
|
+
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id2, attrs: {
|
|
7670
|
+
id: id2,
|
|
7671
|
+
eventId: fact.eventId,
|
|
7672
|
+
kind: "collision",
|
|
7673
|
+
status: "active",
|
|
7674
|
+
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
7675
|
+
updatedAt: now
|
|
7676
|
+
} }], { mutationId: `network-commercial-collision:${fact.eventId}:${fact.digest}` });
|
|
7677
|
+
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
7678
|
+
}
|
|
7679
|
+
const projection = await rebuildNetworkCommercialProjection(
|
|
7680
|
+
db,
|
|
7681
|
+
sources,
|
|
7682
|
+
`network-commercial-project:${fact.eventId}:${fact.digest}`,
|
|
7683
|
+
now
|
|
7684
|
+
);
|
|
7685
|
+
return { status: "stored", projection };
|
|
7686
|
+
}
|
|
7687
|
+
|
|
7688
|
+
// src/network-commercial-reconcile.ts
|
|
7689
|
+
var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
7690
|
+
async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
7691
|
+
const objects = [];
|
|
7692
|
+
let startingAfter;
|
|
7693
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
|
|
7694
|
+
const page = await client.request("GET", path, {
|
|
7695
|
+
...params,
|
|
7696
|
+
limit: 100,
|
|
7697
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
7698
|
+
});
|
|
7699
|
+
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7700
|
+
throw new Error("Stripe returned an invalid commercial list response");
|
|
7701
|
+
}
|
|
7702
|
+
const rows6 = page.data.map(asRaw).filter((row) => row !== null);
|
|
7703
|
+
if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
7704
|
+
objects.push(...rows6);
|
|
7705
|
+
if (!page.has_more) return { objects, complete: true };
|
|
7706
|
+
const cursor = rows6.at(-1)?.id;
|
|
7707
|
+
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7708
|
+
startingAfter = cursor;
|
|
7709
|
+
}
|
|
7710
|
+
return { objects, complete: false };
|
|
7711
|
+
}
|
|
7712
|
+
function facts(snapshots) {
|
|
7713
|
+
const groups = [
|
|
7714
|
+
["customer.updated", snapshots.customers.objects],
|
|
7715
|
+
["customer.subscription.updated", snapshots.subscriptions.objects],
|
|
7716
|
+
["invoice.updated", snapshots.invoices.objects],
|
|
7717
|
+
["refund.updated", snapshots.refunds.objects]
|
|
7718
|
+
];
|
|
7719
|
+
return groups.flatMap(([eventType, objects]) => objects.map((object) => {
|
|
7720
|
+
const id = typeof object.id === "string" ? object.id : "invalid";
|
|
7721
|
+
return normalizeNetworkCommercialStripeEvent({
|
|
7722
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id}`,
|
|
7723
|
+
type: eventType,
|
|
7724
|
+
created: typeof object.created === "number" ? object.created : 0,
|
|
7725
|
+
livemode: object.livemode === true,
|
|
7726
|
+
data: { object }
|
|
7727
|
+
}, `stripe-snapshot:${eventType}:${id}`);
|
|
7728
|
+
}));
|
|
7729
|
+
}
|
|
7730
|
+
var selected = (row) => ({
|
|
7731
|
+
status: row.status ?? null,
|
|
7732
|
+
currency: row.currency ?? null,
|
|
7733
|
+
amountCents: row.amountCents ?? null,
|
|
7734
|
+
amountPaidCents: row.amountPaidCents ?? null,
|
|
7735
|
+
amountRemainingCents: row.amountRemainingCents ?? null,
|
|
7736
|
+
customerId: row.customerId ?? null,
|
|
7737
|
+
subscriptionId: row.subscriptionId ?? null,
|
|
7738
|
+
invoiceId: row.invoiceId ?? null,
|
|
7739
|
+
priceId: row.priceId ?? null,
|
|
7740
|
+
quantity: row.quantity ?? null,
|
|
7741
|
+
applicationId: row.applicationId ?? null,
|
|
7742
|
+
personId: row.personId ?? null,
|
|
7743
|
+
membershipId: row.membershipId ?? null,
|
|
7744
|
+
currentPeriodStart: row.currentPeriodStart ?? null,
|
|
7745
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
7746
|
+
cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
|
|
7747
|
+
});
|
|
7748
|
+
var index = (projection) => new Map(
|
|
7749
|
+
[...projection.customers, ...projection.subscriptions, ...projection.invoices, ...projection.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
|
|
7750
|
+
);
|
|
7751
|
+
function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
7752
|
+
const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
|
|
7753
|
+
const shadowRows = index(shadow);
|
|
7754
|
+
const stripeRows = index(stripe);
|
|
7755
|
+
const exceptions = [];
|
|
7756
|
+
const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
|
|
7757
|
+
if (!complete) exceptions.push({ kind: "stripe_snapshot_truncated", detail: {} });
|
|
7758
|
+
for (const [key, row] of shadowRows) {
|
|
7759
|
+
const other = stripeRows.get(key);
|
|
7760
|
+
if (!other) {
|
|
7761
|
+
exceptions.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
7762
|
+
} else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
|
|
7763
|
+
exceptions.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
|
|
7764
|
+
}
|
|
7765
|
+
}
|
|
7766
|
+
for (const [key, row] of stripeRows) {
|
|
7767
|
+
if (!shadowRows.has(key)) exceptions.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
7768
|
+
}
|
|
7769
|
+
if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
|
|
7770
|
+
exceptions.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
|
|
7771
|
+
}
|
|
7772
|
+
return {
|
|
7773
|
+
ok: exceptions.length === 0,
|
|
7774
|
+
complete,
|
|
7775
|
+
ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
|
|
7776
|
+
exceptions,
|
|
7777
|
+
stripeTotals: stripe.totals
|
|
7778
|
+
};
|
|
7779
|
+
}
|
|
7780
|
+
|
|
7781
|
+
// src/worker-routes-network-commercial.ts
|
|
7782
|
+
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
7783
|
+
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
7784
|
+
var MAX_BODY3 = 512e3;
|
|
7785
|
+
var MUTATION_ID5 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7786
|
+
async function digest(body) {
|
|
7787
|
+
const value = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
7788
|
+
return [...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7789
|
+
}
|
|
7790
|
+
async function boundedBody(req) {
|
|
7791
|
+
if (Number(req.headers.get("content-length") ?? 0) > MAX_BODY3) return null;
|
|
7792
|
+
const body = await req.text();
|
|
7793
|
+
return new TextEncoder().encode(body).byteLength <= MAX_BODY3 ? body : null;
|
|
7794
|
+
}
|
|
7795
|
+
async function gate11(req, env, ctx) {
|
|
7796
|
+
const db = ctx.makeDb(env);
|
|
7797
|
+
const user = await ctx.verifyUser(req, env);
|
|
7798
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7799
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7800
|
+
return { db, user };
|
|
7801
|
+
}
|
|
7802
|
+
function safeProjection(projection) {
|
|
7803
|
+
const row = (item) => ({
|
|
7804
|
+
sourceKey: item.sourceKey,
|
|
7805
|
+
providerId: item.objectId,
|
|
7806
|
+
eventType: item.eventType,
|
|
7807
|
+
eventCreated: item.eventCreated,
|
|
7808
|
+
status: item.status ?? null,
|
|
7809
|
+
currency: item.currency ?? null,
|
|
7810
|
+
amountCents: item.amountCents ?? null,
|
|
7811
|
+
amountPaidCents: item.amountPaidCents ?? null,
|
|
7812
|
+
amountRemainingCents: item.amountRemainingCents ?? null,
|
|
7813
|
+
customerId: item.customerId ?? null,
|
|
7814
|
+
subscriptionId: item.subscriptionId ?? null,
|
|
7815
|
+
invoiceId: item.invoiceId ?? null,
|
|
7816
|
+
priceId: item.priceId ?? null,
|
|
7817
|
+
quantity: item.quantity ?? null,
|
|
7818
|
+
applicationId: item.applicationId ?? null,
|
|
7819
|
+
personId: item.personId ?? null,
|
|
7820
|
+
membershipId: item.membershipId ?? null,
|
|
7821
|
+
currentPeriodStart: item.currentPeriodStart ?? null,
|
|
7822
|
+
currentPeriodEnd: item.currentPeriodEnd ?? null,
|
|
7823
|
+
cancelAtPeriodEnd: item.cancelAtPeriodEnd ?? null
|
|
7824
|
+
});
|
|
7825
|
+
return {
|
|
7826
|
+
version: 1,
|
|
7827
|
+
mode: "shadow",
|
|
7828
|
+
writable: false,
|
|
7829
|
+
authoritativeForCharges: false,
|
|
7830
|
+
totals: projection.totals,
|
|
7831
|
+
customers: projection.customers.map(row),
|
|
7832
|
+
subscriptions: projection.subscriptions.map(row),
|
|
7833
|
+
invoices: projection.invoices.map(row),
|
|
7834
|
+
refunds: projection.refunds.map(row),
|
|
7835
|
+
exceptions: projection.exceptions
|
|
7836
|
+
};
|
|
7837
|
+
}
|
|
7838
|
+
var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
7839
|
+
if (url.pathname !== WEBHOOK_PATH) return null;
|
|
7840
|
+
if (!ctx.chapter.networkCommercialReadModel) return json({ error: "not found" }, 404);
|
|
7841
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7842
|
+
const body = await boundedBody(req);
|
|
7843
|
+
if (body === null) return json({ error: "request is too large" }, 413);
|
|
7844
|
+
const signature = req.headers.get("stripe-signature") ?? "";
|
|
7845
|
+
const db = ctx.makeDb(env);
|
|
7846
|
+
let secret;
|
|
7847
|
+
try {
|
|
7848
|
+
secret = await db.secrets.get("network_commercial_stripe_webhook_secret");
|
|
7849
|
+
} catch {
|
|
7850
|
+
return json({ error: "commercial webhook is not configured" }, 503);
|
|
7851
|
+
}
|
|
7852
|
+
try {
|
|
7853
|
+
const event = await constructStripeEvent(body, signature, secret);
|
|
7854
|
+
const fact = normalizeNetworkCommercialStripeEvent(event, await digest(body));
|
|
7855
|
+
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
7856
|
+
const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact);
|
|
7857
|
+
if (result.status === "collision") {
|
|
7858
|
+
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
7859
|
+
}
|
|
7860
|
+
return json({ ok: true, status: result.status, eventId: fact.eventId });
|
|
7861
|
+
} catch (error) {
|
|
7862
|
+
if (error instanceof StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
7863
|
+
throw error;
|
|
7864
|
+
}
|
|
7865
|
+
};
|
|
7866
|
+
var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
7867
|
+
if (url.pathname !== ADMIN_PATH2) return null;
|
|
7868
|
+
if (!ctx.chapter.networkCommercialReadModel) return json({ error: "not found" }, 404);
|
|
7869
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7870
|
+
const got = await gate11(req, env, ctx);
|
|
7871
|
+
if ("response" in got) return got.response;
|
|
7872
|
+
const db = got.db;
|
|
7873
|
+
if (req.method === "POST") {
|
|
7874
|
+
const body = await boundedBody(req);
|
|
7875
|
+
if (body === null) return json({ error: "request is too large" }, 413);
|
|
7876
|
+
let parsed;
|
|
7877
|
+
try {
|
|
7878
|
+
const value = JSON.parse(body);
|
|
7879
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("shape");
|
|
7880
|
+
parsed = value;
|
|
7881
|
+
} catch {
|
|
7882
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7883
|
+
}
|
|
7884
|
+
if (parsed.command === "reconcile-stripe") {
|
|
7885
|
+
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7886
|
+
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
7887
|
+
try {
|
|
7888
|
+
const stripe = initStripe({ secretKey });
|
|
7889
|
+
const [customers, subscriptions, invoices, refunds] = await Promise.all([
|
|
7890
|
+
listStripeObjects(stripe, "/v1/customers"),
|
|
7891
|
+
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
7892
|
+
listStripeObjects(stripe, "/v1/invoices"),
|
|
7893
|
+
listStripeObjects(stripe, "/v1/refunds")
|
|
7894
|
+
]);
|
|
7895
|
+
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
7896
|
+
return json({
|
|
7897
|
+
reconciled: true,
|
|
7898
|
+
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds }, ctx.chapter.networkCommercialReadModel.sources)
|
|
7899
|
+
});
|
|
7900
|
+
} catch (error) {
|
|
7901
|
+
if (error instanceof StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
7902
|
+
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
7906
|
+
return json({ error: "rebuild or reconcile-stripe command is required; rebuild also requires a valid mutationId" }, 400);
|
|
7907
|
+
}
|
|
7908
|
+
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
7909
|
+
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
7910
|
+
}
|
|
7911
|
+
return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
|
|
7912
|
+
};
|
|
7913
|
+
|
|
6728
7914
|
// src/worker-routes-formation.ts
|
|
6729
7915
|
import { createRecord as createRecord3 } from "@odla-ai/crm";
|
|
6730
7916
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
@@ -6741,11 +7927,11 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
6741
7927
|
});
|
|
6742
7928
|
}
|
|
6743
7929
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|
|
6744
|
-
const
|
|
6745
|
-
if (
|
|
7930
|
+
const raw2 = await req.text();
|
|
7931
|
+
if (raw2.length > formation.bodyCap) return json({ error: "request body too large" }, 413);
|
|
6746
7932
|
let body;
|
|
6747
7933
|
try {
|
|
6748
|
-
body = JSON.parse(
|
|
7934
|
+
body = JSON.parse(raw2);
|
|
6749
7935
|
} catch {
|
|
6750
7936
|
return json({ error: "invalid JSON body" }, 400);
|
|
6751
7937
|
}
|
|
@@ -6829,7 +8015,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
6829
8015
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
6830
8016
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
6831
8017
|
}
|
|
6832
|
-
function
|
|
8018
|
+
function text4(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
6833
8019
|
if (value === void 0 && !required) return void 0;
|
|
6834
8020
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
6835
8021
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -6839,23 +8025,23 @@ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
|
6839
8025
|
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
6840
8026
|
return result;
|
|
6841
8027
|
}
|
|
6842
|
-
function
|
|
8028
|
+
function integer3(value, path, errors, minimum = 1) {
|
|
6843
8029
|
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
6844
8030
|
return Number(value);
|
|
6845
8031
|
}
|
|
6846
8032
|
function scopeValue(value, path, errors) {
|
|
6847
|
-
const
|
|
6848
|
-
if (!
|
|
8033
|
+
const raw2 = record3(value);
|
|
8034
|
+
if (!raw2) {
|
|
6849
8035
|
errors.push(`${path} must be an object`);
|
|
6850
8036
|
return null;
|
|
6851
8037
|
}
|
|
6852
|
-
unknownKeys3(
|
|
6853
|
-
if (
|
|
6854
|
-
if (
|
|
6855
|
-
return { kind:
|
|
8038
|
+
unknownKeys3(raw2, SCOPE_KEYS, path, errors);
|
|
8039
|
+
if (raw2.kind === "global" || raw2.kind === "built_not_found") {
|
|
8040
|
+
if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
8041
|
+
return { kind: raw2.kind };
|
|
6856
8042
|
}
|
|
6857
|
-
if (
|
|
6858
|
-
const chapterId =
|
|
8043
|
+
if (raw2.kind === "chapter") {
|
|
8044
|
+
const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
6859
8045
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
6860
8046
|
return { kind: "chapter", chapterId };
|
|
6861
8047
|
}
|
|
@@ -6863,28 +8049,28 @@ function scopeValue(value, path, errors) {
|
|
|
6863
8049
|
return null;
|
|
6864
8050
|
}
|
|
6865
8051
|
function resourceValue(value, path, errors) {
|
|
6866
|
-
const
|
|
6867
|
-
if (!
|
|
8052
|
+
const raw2 = record3(value);
|
|
8053
|
+
if (!raw2) {
|
|
6868
8054
|
errors.push(`${path} must be an object`);
|
|
6869
8055
|
return null;
|
|
6870
8056
|
}
|
|
6871
|
-
unknownKeys3(
|
|
6872
|
-
const id =
|
|
8057
|
+
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
8058
|
+
const id = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
6873
8059
|
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
6874
|
-
const kind =
|
|
8060
|
+
const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
6875
8061
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
6876
|
-
const title =
|
|
6877
|
-
const summary =
|
|
6878
|
-
const status =
|
|
8062
|
+
const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
8063
|
+
const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
8064
|
+
const status = text4(raw2.status, `${path}.status`, errors) ?? "";
|
|
6879
8065
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
6880
|
-
const revision =
|
|
6881
|
-
const scope = scopeValue(
|
|
6882
|
-
const tags = Array.isArray(
|
|
6883
|
-
if (!Array.isArray(
|
|
8066
|
+
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
8067
|
+
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
8068
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
8069
|
+
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
6884
8070
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
6885
8071
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
6886
|
-
const bodyMarkdown =
|
|
6887
|
-
const url =
|
|
8072
|
+
const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
8073
|
+
const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
|
|
6888
8074
|
if (url) {
|
|
6889
8075
|
try {
|
|
6890
8076
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -6892,13 +8078,13 @@ function resourceValue(value, path, errors) {
|
|
|
6892
8078
|
errors.push(`${path}.url must be a valid https URL`);
|
|
6893
8079
|
}
|
|
6894
8080
|
}
|
|
6895
|
-
const asset =
|
|
6896
|
-
if (
|
|
8081
|
+
const asset = raw2.brandAsset === void 0 ? null : record3(raw2.brandAsset);
|
|
8082
|
+
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
6897
8083
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
6898
8084
|
const brandAsset = asset ? {
|
|
6899
|
-
bookId:
|
|
6900
|
-
assetId:
|
|
6901
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
8085
|
+
bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
8086
|
+
assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
8087
|
+
...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
6902
8088
|
} : void 0;
|
|
6903
8089
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
6904
8090
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -6906,58 +8092,58 @@ function resourceValue(value, path, errors) {
|
|
|
6906
8092
|
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
6907
8093
|
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
6908
8094
|
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
6909
|
-
const provenanceRaw = record3(
|
|
8095
|
+
const provenanceRaw = record3(raw2.provenance);
|
|
6910
8096
|
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
6911
8097
|
if (provenanceRaw) unknownKeys3(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
6912
8098
|
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record3(provenanceRaw.promotedFrom);
|
|
6913
8099
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
6914
8100
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
6915
8101
|
const promotedFrom = promotedRaw ? {
|
|
6916
|
-
chapterId:
|
|
6917
|
-
resourceId:
|
|
6918
|
-
resourceRevision:
|
|
6919
|
-
documentRevision:
|
|
6920
|
-
promotedBy:
|
|
6921
|
-
promotedAt:
|
|
8102
|
+
chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
8103
|
+
resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
8104
|
+
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
8105
|
+
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
8106
|
+
promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
8107
|
+
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
6922
8108
|
} : void 0;
|
|
6923
8109
|
const provenance = {
|
|
6924
|
-
createdBy:
|
|
6925
|
-
createdAt:
|
|
6926
|
-
updatedBy:
|
|
6927
|
-
updatedAt:
|
|
8110
|
+
createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
8111
|
+
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
8112
|
+
updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
8113
|
+
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
6928
8114
|
...promotedFrom ? { promotedFrom } : {}
|
|
6929
8115
|
};
|
|
6930
8116
|
return { id, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
6931
8117
|
}
|
|
6932
8118
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
6933
8119
|
const errors = [];
|
|
6934
|
-
const
|
|
6935
|
-
if (!
|
|
6936
|
-
unknownKeys3(
|
|
6937
|
-
if (
|
|
6938
|
-
const revision =
|
|
6939
|
-
const generatedAt =
|
|
6940
|
-
const sourceRaw = record3(
|
|
8120
|
+
const raw2 = record3(value);
|
|
8121
|
+
if (!raw2) return { ok: false, errors: ["document must be an object"] };
|
|
8122
|
+
unknownKeys3(raw2, ROOT_KEYS4, "document", errors);
|
|
8123
|
+
if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
8124
|
+
const revision = integer3(raw2.revision, "document.revision", errors);
|
|
8125
|
+
const generatedAt = integer3(raw2.generatedAt, "document.generatedAt", errors, 0);
|
|
8126
|
+
const sourceRaw = record3(raw2.source);
|
|
6941
8127
|
if (!sourceRaw) errors.push("document.source must be an object");
|
|
6942
8128
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
6943
8129
|
const sourceType = sourceRaw?.type;
|
|
6944
8130
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
6945
|
-
const sourceAppId =
|
|
8131
|
+
const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
6946
8132
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6947
8133
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
6948
8134
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
6949
|
-
const targetRaw =
|
|
6950
|
-
if (
|
|
8135
|
+
const targetRaw = raw2.target === void 0 ? null : record3(raw2.target);
|
|
8136
|
+
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
6951
8137
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
6952
8138
|
const target = targetRaw ? {
|
|
6953
|
-
appId:
|
|
6954
|
-
environment:
|
|
8139
|
+
appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
8140
|
+
environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
6955
8141
|
} : void 0;
|
|
6956
8142
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
6957
|
-
if (!Array.isArray(
|
|
6958
|
-
const inputs = Array.isArray(
|
|
8143
|
+
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
8144
|
+
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
6959
8145
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
6960
|
-
const resources = inputs.map((item,
|
|
8146
|
+
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
6961
8147
|
const ids = resources.map(({ id }) => id);
|
|
6962
8148
|
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
6963
8149
|
for (const resource of resources) {
|
|
@@ -7031,28 +8217,28 @@ function canonicalBuilderLibrary(value) {
|
|
|
7031
8217
|
return canonicalValue3(value);
|
|
7032
8218
|
}
|
|
7033
8219
|
async function builderLibraryDigest(document) {
|
|
7034
|
-
const
|
|
7035
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
8220
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalBuilderLibrary(document)));
|
|
8221
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
7036
8222
|
}
|
|
7037
8223
|
|
|
7038
8224
|
// src/builder-library-store.ts
|
|
7039
8225
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7040
8226
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7041
8227
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
7042
|
-
const
|
|
7043
|
-
return Array.isArray(
|
|
8228
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
8229
|
+
return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
|
|
7044
8230
|
}
|
|
7045
8231
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
7046
|
-
const
|
|
7047
|
-
return control2(Array.isArray(
|
|
8232
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
8233
|
+
return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7048
8234
|
}
|
|
7049
8235
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
7050
8236
|
const id = `builder-library-local:${environment}`;
|
|
7051
|
-
const
|
|
7052
|
-
return local2(Array.isArray(
|
|
8237
|
+
const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
8238
|
+
return local2(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
7053
8239
|
}
|
|
7054
8240
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
7055
|
-
const { document, digest } = envelope;
|
|
8241
|
+
const { document, digest: digest2 } = envelope;
|
|
7056
8242
|
const target = document.target;
|
|
7057
8243
|
const id = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
7058
8244
|
const ops = [];
|
|
@@ -7063,12 +8249,12 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
7063
8249
|
targetId: target.appId,
|
|
7064
8250
|
environment: target.environment,
|
|
7065
8251
|
revision: document.revision,
|
|
7066
|
-
digest,
|
|
8252
|
+
digest: digest2,
|
|
7067
8253
|
document,
|
|
7068
8254
|
active: true,
|
|
7069
8255
|
receivedAt
|
|
7070
8256
|
} });
|
|
7071
|
-
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${
|
|
8257
|
+
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest2}` });
|
|
7072
8258
|
}
|
|
7073
8259
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7074
8260
|
const id = `builder-library-local:${environment}`;
|
|
@@ -7121,7 +8307,7 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
|
7121
8307
|
} catch {
|
|
7122
8308
|
return json({ ok: false, reason: "invalid_document", errors: ["request body must be JSON"] }, 400);
|
|
7123
8309
|
}
|
|
7124
|
-
const
|
|
8310
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
7125
8311
|
const sourceType = ctx.chapter.mode === "hub" ? "chapter" : "built_not_found";
|
|
7126
8312
|
const checked = validateBuilderLibraryDocument(input.document, {
|
|
7127
8313
|
sourceType,
|
|
@@ -7135,29 +8321,29 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
|
7135
8321
|
return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global or this Chapter's resources"] }, 400);
|
|
7136
8322
|
}
|
|
7137
8323
|
if (!document.target || document.target.environment !== env.ODLA_ENV) return json({ ok: false, reason: "wrong_environment" }, 409);
|
|
7138
|
-
if (!
|
|
8324
|
+
if (!digest2 || await builderLibraryDigest(document) !== digest2) return json({ ok: false, reason: "invalid_digest" }, 400);
|
|
7139
8325
|
const latest = await loadLatestBuilderLibraryControl(db, document.source.appId, ctx.chapter.id, env.ODLA_ENV);
|
|
7140
8326
|
if (latest && document.revision < latest.revision) return json({ ok: false, reason: "stale_revision" }, 409);
|
|
7141
8327
|
if (latest && document.revision === latest.revision) {
|
|
7142
|
-
if (latest.digest !==
|
|
8328
|
+
if (latest.digest !== digest2) return json({ ok: false, reason: "conflicting_revision" }, 409);
|
|
7143
8329
|
return json({ ok: true, target: document.target, revision: latest.revision, digest: latest.digest, status: "replayed", acknowledgedAt: latest.receivedAt });
|
|
7144
8330
|
}
|
|
7145
8331
|
const receivedAt = Date.now();
|
|
7146
8332
|
try {
|
|
7147
|
-
await materializeBuilderLibraryControl(db, { document, digest }, latest, receivedAt);
|
|
8333
|
+
await materializeBuilderLibraryControl(db, { document, digest: digest2 }, latest, receivedAt);
|
|
7148
8334
|
} catch {
|
|
7149
8335
|
return json({ ok: false, reason: "materialization_failed" }, 503);
|
|
7150
8336
|
}
|
|
7151
|
-
return json({ ok: true, target: document.target, revision: document.revision, digest, status: "accepted", acknowledgedAt: receivedAt });
|
|
8337
|
+
return json({ ok: true, target: document.target, revision: document.revision, digest: digest2, status: "accepted", acknowledgedAt: receivedAt });
|
|
7152
8338
|
};
|
|
7153
8339
|
function actorFor(user) {
|
|
7154
8340
|
return user.email ?? user.userId;
|
|
7155
8341
|
}
|
|
7156
8342
|
function withLocalAuthority2(value, mode, appId, revision, now, actor, current) {
|
|
7157
8343
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
7158
|
-
const
|
|
8344
|
+
const raw2 = value;
|
|
7159
8345
|
const currentById = new Map((current?.resources ?? []).map((item) => [item.id, item]));
|
|
7160
|
-
const resources = Array.isArray(
|
|
8346
|
+
const resources = Array.isArray(raw2.resources) ? raw2.resources.map((input) => {
|
|
7161
8347
|
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
7162
8348
|
const item = input;
|
|
7163
8349
|
const existing = typeof item.id === "string" ? currentById.get(item.id) : void 0;
|
|
@@ -7172,9 +8358,9 @@ function withLocalAuthority2(value, mode, appId, revision, now, actor, current)
|
|
|
7172
8358
|
...existing?.provenance.promotedFrom ? { promotedFrom: existing.provenance.promotedFrom } : {}
|
|
7173
8359
|
}
|
|
7174
8360
|
};
|
|
7175
|
-
}) :
|
|
8361
|
+
}) : raw2.resources;
|
|
7176
8362
|
return {
|
|
7177
|
-
...
|
|
8363
|
+
...raw2,
|
|
7178
8364
|
schemaVersion: 1,
|
|
7179
8365
|
revision,
|
|
7180
8366
|
generatedAt: now,
|
|
@@ -7255,7 +8441,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
7255
8441
|
return json({
|
|
7256
8442
|
mode: ctx.chapter.mode,
|
|
7257
8443
|
local: local3 ? { revision: local3.revision, document: local3.document } : null,
|
|
7258
|
-
sources: controls.map(({ sourceId, revision, digest, document, receivedAt }) => ({ sourceId, revision, digest, document, receivedAt })),
|
|
8444
|
+
sources: controls.map(({ sourceId, revision, digest: digest2, document, receivedAt }) => ({ sourceId, revision, digest: digest2, document, receivedAt })),
|
|
7259
8445
|
localAuthoring: ctx.chapter.builderLibrary.localAuthoring,
|
|
7260
8446
|
chapterTargets: ctx.chapter.mode === "hub" ? ctx.chapter.builderLibrary.sources.map((source) => ({ chapterId: source.sourceId })) : [{ chapterId: ctx.chapter.id }],
|
|
7261
8447
|
visibility: { builtNotFoundSeesChapterSources: ctx.chapter.mode === "hub", chaptersSeeSiblingSources: false }
|
|
@@ -7263,11 +8449,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
7263
8449
|
}
|
|
7264
8450
|
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
7265
8451
|
if (!ctx.chapter.builderLibrary.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
7266
|
-
const
|
|
7267
|
-
if (
|
|
8452
|
+
const raw2 = await req.text();
|
|
8453
|
+
if (raw2.length > BODY_CAP2) return json({ error: "request body too large" }, 413);
|
|
7268
8454
|
let input;
|
|
7269
8455
|
try {
|
|
7270
|
-
input = JSON.parse(
|
|
8456
|
+
input = JSON.parse(raw2);
|
|
7271
8457
|
} catch {
|
|
7272
8458
|
return json({ error: "invalid JSON body" }, 400);
|
|
7273
8459
|
}
|
|
@@ -7372,14 +8558,14 @@ function validatedTokens(input) {
|
|
|
7372
8558
|
}
|
|
7373
8559
|
const tokens = {};
|
|
7374
8560
|
let serializedLength = 0;
|
|
7375
|
-
for (const [name,
|
|
8561
|
+
for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
7376
8562
|
if (!TOKEN_NAME.test(name)) {
|
|
7377
8563
|
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
7378
8564
|
}
|
|
7379
|
-
if (typeof
|
|
8565
|
+
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
7380
8566
|
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
7381
8567
|
}
|
|
7382
|
-
const value = String(
|
|
8568
|
+
const value = String(raw2).trim();
|
|
7383
8569
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
7384
8570
|
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
7385
8571
|
}
|
|
@@ -7501,6 +8687,7 @@ var BUILTIN_ROUTES = [
|
|
|
7501
8687
|
handleNetworkSnapshot,
|
|
7502
8688
|
handleNetworkRecords,
|
|
7503
8689
|
handleNetworkSharedNotes,
|
|
8690
|
+
handleNetworkCommercialStripeWebhook,
|
|
7504
8691
|
handleFormation,
|
|
7505
8692
|
handleMember,
|
|
7506
8693
|
handleMemberContent,
|
|
@@ -7540,6 +8727,8 @@ var BUILTIN_ROUTES = [
|
|
|
7540
8727
|
handleAdminNetworkRecords,
|
|
7541
8728
|
handleAdminNetworkNotes,
|
|
7542
8729
|
handleAdminNetworkSharedNotes,
|
|
8730
|
+
handleAdminNetworkPersonRegistry,
|
|
8731
|
+
handleAdminNetworkCommercialReadModel,
|
|
7543
8732
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
7544
8733
|
// get first refusal through options.routes, then this terminates unknown API
|
|
7545
8734
|
// paths with an explicit machine-readable 404.
|