@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.cjs
CHANGED
|
@@ -39,8 +39,8 @@ var import_jose = require("jose");
|
|
|
39
39
|
|
|
40
40
|
// src/auth.ts
|
|
41
41
|
function roleFromClaim(payload, auth) {
|
|
42
|
-
const
|
|
43
|
-
return typeof
|
|
42
|
+
const raw2 = payload[auth.claim];
|
|
43
|
+
return typeof raw2 === "string" && auth.ladder.includes(raw2) ? raw2 : auth.ladder[0];
|
|
44
44
|
}
|
|
45
45
|
function isAdminRole(role, auth) {
|
|
46
46
|
return role === auth.adminRole;
|
|
@@ -223,7 +223,7 @@ function resolveApplication(a) {
|
|
|
223
223
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
-
const duplicate = [...required, ...optional].find((field,
|
|
226
|
+
const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
|
|
227
227
|
if (duplicate) {
|
|
228
228
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
229
229
|
}
|
|
@@ -371,8 +371,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
371
371
|
function byOrder(a, b) {
|
|
372
372
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
373
373
|
}
|
|
374
|
-
function resolveTiers(group,
|
|
375
|
-
const stored =
|
|
374
|
+
function resolveTiers(group, rows6 = []) {
|
|
375
|
+
const stored = rows6.filter((tier) => tier.active);
|
|
376
376
|
if (stored.length) return [...stored].sort(byOrder);
|
|
377
377
|
return [{
|
|
378
378
|
id: "standard",
|
|
@@ -418,15 +418,15 @@ function rowToTier(row, groupId) {
|
|
|
418
418
|
}
|
|
419
419
|
async function loadTiers(db, group) {
|
|
420
420
|
const groupId = String(group.id ?? "");
|
|
421
|
-
let
|
|
421
|
+
let rows6;
|
|
422
422
|
try {
|
|
423
|
-
|
|
423
|
+
rows6 = (await db.query({
|
|
424
424
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
425
425
|
})).tiers;
|
|
426
426
|
} catch {
|
|
427
|
-
|
|
427
|
+
rows6 = void 0;
|
|
428
428
|
}
|
|
429
|
-
const list = Array.isArray(
|
|
429
|
+
const list = Array.isArray(rows6) ? rows6.map((row) => rowToTier(row, groupId)) : [];
|
|
430
430
|
return resolveTiers(group, list);
|
|
431
431
|
}
|
|
432
432
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -563,8 +563,8 @@ function canonicalSignupControl(value) {
|
|
|
563
563
|
return canonicalValue(value);
|
|
564
564
|
}
|
|
565
565
|
async function signupControlDigest(document) {
|
|
566
|
-
const
|
|
567
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
566
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
567
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
// src/admission-grants.ts
|
|
@@ -600,24 +600,24 @@ function validateAdmissionGrantDocument(input) {
|
|
|
600
600
|
const grants = [];
|
|
601
601
|
const seenIds = /* @__PURE__ */ new Set();
|
|
602
602
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
603
|
-
for (const [
|
|
604
|
-
const grant =
|
|
603
|
+
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
604
|
+
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
605
605
|
const id = typeof grant.id === "string" ? grant.id : "";
|
|
606
606
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
607
607
|
const normalized = normalizeAdmissionEmail(email);
|
|
608
608
|
const decision = grant.decision;
|
|
609
609
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
610
610
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
611
|
-
if (!ID.test(id)) errors.push(`document.grants[${
|
|
611
|
+
if (!ID.test(id)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
612
612
|
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
613
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${
|
|
613
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
614
614
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
615
615
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
616
|
-
errors.push(`document.grants[${
|
|
616
|
+
errors.push(`document.grants[${index2}].decision is invalid`);
|
|
617
617
|
}
|
|
618
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${
|
|
618
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index2}].note is too long`);
|
|
619
619
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
620
|
-
errors.push(`document.grants[${
|
|
620
|
+
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
621
621
|
}
|
|
622
622
|
seenIds.add(id);
|
|
623
623
|
seenEmails.add(normalized);
|
|
@@ -685,7 +685,7 @@ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment)
|
|
|
685
685
|
return row ? row : null;
|
|
686
686
|
}
|
|
687
687
|
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
688
|
-
const { document, digest } = envelope;
|
|
688
|
+
const { document, digest: digest2 } = envelope;
|
|
689
689
|
const current = rows((await db.query({ admissionGrants: { $: {
|
|
690
690
|
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
691
691
|
limit: MAX_GRANTS
|
|
@@ -737,11 +737,11 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
737
737
|
targetId: document.target.chapterId,
|
|
738
738
|
environment: document.target.environment,
|
|
739
739
|
revision: document.revision,
|
|
740
|
-
digest,
|
|
740
|
+
digest: digest2,
|
|
741
741
|
active: true,
|
|
742
742
|
receivedAt
|
|
743
743
|
} });
|
|
744
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${
|
|
744
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest2}` });
|
|
745
745
|
}
|
|
746
746
|
|
|
747
747
|
// src/flow-fields.ts
|
|
@@ -759,8 +759,8 @@ var APPLICATION_LABELS = {
|
|
|
759
759
|
state: "State"
|
|
760
760
|
};
|
|
761
761
|
function humanize(id) {
|
|
762
|
-
const
|
|
763
|
-
return
|
|
762
|
+
const text5 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
763
|
+
return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id;
|
|
764
764
|
}
|
|
765
765
|
function applicationDefs(chapter) {
|
|
766
766
|
try {
|
|
@@ -775,7 +775,7 @@ function applicationType(id, def) {
|
|
|
775
775
|
}
|
|
776
776
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
777
777
|
const defs = applicationDefs(chapter);
|
|
778
|
-
return [...application.required, ...application.optional].map((id,
|
|
778
|
+
return [...application.required, ...application.optional].map((id, order2) => {
|
|
779
779
|
const def = defs[id];
|
|
780
780
|
const conditions = application.conditions[id];
|
|
781
781
|
const type = applicationType(id, def);
|
|
@@ -784,7 +784,7 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
784
784
|
label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
|
|
785
785
|
type,
|
|
786
786
|
required: application.required.includes(id),
|
|
787
|
-
order,
|
|
787
|
+
order: order2,
|
|
788
788
|
maxLength: application.maxLen[id] ?? application.defaultMaxLen,
|
|
789
789
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
790
790
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
@@ -795,14 +795,14 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
795
795
|
function resolveFormationFlowFields(crm, formation) {
|
|
796
796
|
if (!formation.enabled) return [];
|
|
797
797
|
const def = crm.type(formation.type);
|
|
798
|
-
return [...formation.required, ...formation.optional].map((id,
|
|
798
|
+
return [...formation.required, ...formation.optional].map((id, order2) => {
|
|
799
799
|
const field = def.fields[id];
|
|
800
800
|
return {
|
|
801
801
|
id,
|
|
802
802
|
label: field.label ?? humanize(id),
|
|
803
803
|
type: field.type,
|
|
804
804
|
required: formation.required.includes(id),
|
|
805
|
-
order,
|
|
805
|
+
order: order2,
|
|
806
806
|
maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
|
|
807
807
|
...field.options ? { options: [...field.options] } : {},
|
|
808
808
|
...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -846,9 +846,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
846
846
|
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
847
847
|
const ids = /* @__PURE__ */ new Set();
|
|
848
848
|
const offers = [];
|
|
849
|
-
for (const [
|
|
850
|
-
const path = `document.giftOffers.offers[${
|
|
851
|
-
const item = record(
|
|
849
|
+
for (const [index2, raw2] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
850
|
+
const path = `document.giftOffers.offers[${index2}]`;
|
|
851
|
+
const item = record(raw2);
|
|
852
852
|
if (!item) {
|
|
853
853
|
errors.push(`${path} must be an object`);
|
|
854
854
|
continue;
|
|
@@ -1005,8 +1005,8 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1005
1005
|
const typeKey = type.toLowerCase();
|
|
1006
1006
|
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
1007
1007
|
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
1008
|
-
const
|
|
1009
|
-
if (readable &&
|
|
1008
|
+
const raw2 = `${prefix}:${hubRecordId}`;
|
|
1009
|
+
if (readable && raw2.length <= 64) return raw2;
|
|
1010
1010
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1011
1011
|
}
|
|
1012
1012
|
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
@@ -1290,10 +1290,10 @@ function planDelivery(input) {
|
|
|
1290
1290
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1291
1291
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1292
1292
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1293
|
-
const
|
|
1293
|
+
const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1294
1294
|
|
|
1295
1295
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1296
|
-
return { deliver: true, transport, to, subject, text:
|
|
1296
|
+
return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
|
|
1297
1297
|
}
|
|
1298
1298
|
|
|
1299
1299
|
// src/notify.ts
|
|
@@ -1480,27 +1480,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1480
1480
|
if (db.aggregate) {
|
|
1481
1481
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1482
1482
|
}
|
|
1483
|
-
const
|
|
1484
|
-
return Array.isArray(
|
|
1483
|
+
const rows6 = (await db.query({ signupControls: {} })).signupControls;
|
|
1484
|
+
return Array.isArray(rows6) && rows6.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1485
1485
|
}
|
|
1486
1486
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1487
1487
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1488
|
-
const
|
|
1488
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
1489
1489
|
where: { targetId, environment },
|
|
1490
1490
|
order: { revision: "desc" },
|
|
1491
1491
|
limit: 1
|
|
1492
1492
|
} } })).signupControls;
|
|
1493
|
-
const latest = Array.isArray(
|
|
1493
|
+
const latest = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1494
1494
|
return asStored(latest);
|
|
1495
1495
|
}
|
|
1496
1496
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1497
1497
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1498
|
-
const
|
|
1498
|
+
const rows6 = (await db.query({ signupControls: { $: {
|
|
1499
1499
|
where: { targetId, environment, active: true },
|
|
1500
1500
|
order: { revision: "desc" },
|
|
1501
1501
|
limit: 1
|
|
1502
1502
|
} } })).signupControls;
|
|
1503
|
-
const active = Array.isArray(
|
|
1503
|
+
const active = Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1504
1504
|
return asStored(active);
|
|
1505
1505
|
}
|
|
1506
1506
|
function controlledApplication(base, document) {
|
|
@@ -1529,7 +1529,7 @@ var byPublicId = (ns, value) => ({
|
|
|
1529
1529
|
value
|
|
1530
1530
|
});
|
|
1531
1531
|
async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
1532
|
-
const { document, digest } = envelope;
|
|
1532
|
+
const { document, digest: digest2 } = envelope;
|
|
1533
1533
|
const currentRows = (await db.query({ tiers: { $: {
|
|
1534
1534
|
where: { groupId: document.target.chapterId },
|
|
1535
1535
|
limit: 100
|
|
@@ -1580,12 +1580,12 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
1580
1580
|
targetId: document.target.chapterId,
|
|
1581
1581
|
environment: document.target.environment,
|
|
1582
1582
|
revision: document.revision,
|
|
1583
|
-
digest,
|
|
1583
|
+
digest: digest2,
|
|
1584
1584
|
document,
|
|
1585
1585
|
active: true,
|
|
1586
1586
|
receivedAt
|
|
1587
1587
|
} });
|
|
1588
|
-
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${
|
|
1588
|
+
await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest2}` });
|
|
1589
1589
|
}
|
|
1590
1590
|
|
|
1591
1591
|
// src/session.ts
|
|
@@ -2025,11 +2025,11 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
2025
2025
|
return json({ error: `application cannot resume from status "${String(app.status ?? "")}"` }, 409);
|
|
2026
2026
|
}
|
|
2027
2027
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2028
|
+
const raw2 = await req.text();
|
|
2029
|
+
if (raw2.length > chapter.application.bodyCap) return json({ error: "request body too large" }, 413);
|
|
2030
2030
|
let parsed;
|
|
2031
2031
|
try {
|
|
2032
|
-
parsed = JSON.parse(
|
|
2032
|
+
parsed = JSON.parse(raw2);
|
|
2033
2033
|
} catch {
|
|
2034
2034
|
return json({ error: "invalid JSON body" }, 400);
|
|
2035
2035
|
}
|
|
@@ -2086,23 +2086,23 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
2086
2086
|
const db = ctx.makeDb(env);
|
|
2087
2087
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2088
2088
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2089
|
-
let
|
|
2089
|
+
let raw2;
|
|
2090
2090
|
let sourceId;
|
|
2091
2091
|
if (req.headers.has(import_db2.FEDERATION_HEADERS.version)) {
|
|
2092
2092
|
const verified = await (0, import_db2.verifyFederatedRequest)(req, { secret });
|
|
2093
2093
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2094
|
-
|
|
2094
|
+
raw2 = verified.body;
|
|
2095
2095
|
sourceId = verified.sender;
|
|
2096
2096
|
} else {
|
|
2097
2097
|
const provided = (req.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
2098
2098
|
if (provided.length !== secret.length || provided !== secret) {
|
|
2099
2099
|
return json({ error: "unauthorized" }, 401);
|
|
2100
2100
|
}
|
|
2101
|
-
|
|
2101
|
+
raw2 = await req.text();
|
|
2102
2102
|
}
|
|
2103
2103
|
let payload;
|
|
2104
2104
|
try {
|
|
2105
|
-
payload = JSON.parse(
|
|
2105
|
+
payload = JSON.parse(raw2);
|
|
2106
2106
|
} catch {
|
|
2107
2107
|
return json({ error: "invalid JSON body" }, 400);
|
|
2108
2108
|
}
|
|
@@ -2181,10 +2181,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2181
2181
|
errors.push(`${path} must be a string`);
|
|
2182
2182
|
return void 0;
|
|
2183
2183
|
}
|
|
2184
|
-
const
|
|
2185
|
-
if (required && !
|
|
2186
|
-
if (
|
|
2187
|
-
return
|
|
2184
|
+
const text5 = value.trim();
|
|
2185
|
+
if (required && !text5) errors.push(`${path} is required`);
|
|
2186
|
+
if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2187
|
+
return text5;
|
|
2188
2188
|
}
|
|
2189
2189
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2190
2190
|
const errors = [];
|
|
@@ -2202,9 +2202,9 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2202
2202
|
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
2203
2203
|
const seen = /* @__PURE__ */ new Set();
|
|
2204
2204
|
const tiers = [];
|
|
2205
|
-
for (const [
|
|
2206
|
-
const path = `document.tiers[${
|
|
2207
|
-
const item = objectRecord(
|
|
2205
|
+
for (const [index2, raw2] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
2206
|
+
const path = `document.tiers[${index2}]`;
|
|
2207
|
+
const item = objectRecord(raw2);
|
|
2208
2208
|
if (!item) {
|
|
2209
2209
|
errors.push(`${path} must be an object`);
|
|
2210
2210
|
continue;
|
|
@@ -2233,8 +2233,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2233
2233
|
else {
|
|
2234
2234
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2235
2235
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2236
|
-
const
|
|
2237
|
-
return
|
|
2236
|
+
const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2237
|
+
return text5 ? [text5] : [];
|
|
2238
2238
|
});
|
|
2239
2239
|
}
|
|
2240
2240
|
}
|
|
@@ -2343,9 +2343,9 @@ function tiersOf(value, errors) {
|
|
|
2343
2343
|
}
|
|
2344
2344
|
if (value.length > 20) errors.push("document.tiers supports at most 20 entries");
|
|
2345
2345
|
const seen = /* @__PURE__ */ new Set();
|
|
2346
|
-
return value.slice(0, 20).flatMap((
|
|
2347
|
-
const path = `document.tiers[${
|
|
2348
|
-
const item = recordOf(
|
|
2346
|
+
return value.slice(0, 20).flatMap((raw2, index2) => {
|
|
2347
|
+
const path = `document.tiers[${index2}]`;
|
|
2348
|
+
const item = recordOf(raw2);
|
|
2349
2349
|
if (!item) {
|
|
2350
2350
|
errors.push(`${path} must be an object`);
|
|
2351
2351
|
return [];
|
|
@@ -2472,7 +2472,7 @@ function validateSignupControlDocument(input) {
|
|
|
2472
2472
|
const available = tiers.filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
2473
2473
|
const page = validateMembershipPageDocument(root.membershipPage, { tiers: available });
|
|
2474
2474
|
if (!page.ok) errors.push(...page.errors);
|
|
2475
|
-
else if (page.value.tiers.some((tier,
|
|
2475
|
+
else if (page.value.tiers.some((tier, index2) => tier.tierId !== available[index2]?.id)) {
|
|
2476
2476
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
2477
2477
|
}
|
|
2478
2478
|
const application = applicationOf(root.application, errors);
|
|
@@ -2520,7 +2520,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2520
2520
|
} catch {
|
|
2521
2521
|
return reject("invalid_document", 400, ["request body must be JSON"]);
|
|
2522
2522
|
}
|
|
2523
|
-
const
|
|
2523
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
2524
2524
|
const checked = validateSignupControlDocument(input.document);
|
|
2525
2525
|
if (!checked.ok) return reject("invalid_document", 400, checked.errors);
|
|
2526
2526
|
const document = checked.value;
|
|
@@ -2530,11 +2530,11 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2530
2530
|
if (document.target.stripeMode !== authority.stripeMode) return reject("wrong_stripe_mode", 409);
|
|
2531
2531
|
const applicationErrors = validateControlledApplication(ctx.chapter, document);
|
|
2532
2532
|
if (applicationErrors.length) return reject("invalid_document", 400, applicationErrors);
|
|
2533
|
-
if (!
|
|
2533
|
+
if (!digest2 || await signupControlDigest(document) !== digest2) return reject("invalid_digest", 400);
|
|
2534
2534
|
const latest = await loadLatestSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
2535
2535
|
if (latest && document.revision < latest.revision) return reject("stale_revision", 409);
|
|
2536
2536
|
if (latest && document.revision === latest.revision) {
|
|
2537
|
-
if (latest.digest !==
|
|
2537
|
+
if (latest.digest !== digest2) return reject("conflicting_revision", 409);
|
|
2538
2538
|
return json({
|
|
2539
2539
|
ok: true,
|
|
2540
2540
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -2546,7 +2546,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2546
2546
|
}
|
|
2547
2547
|
const receivedAt = Date.now();
|
|
2548
2548
|
try {
|
|
2549
|
-
await materializeSignupControl(db, { digest, document }, latest, receivedAt);
|
|
2549
|
+
await materializeSignupControl(db, { digest: digest2, document }, latest, receivedAt);
|
|
2550
2550
|
} catch {
|
|
2551
2551
|
return reject("materialization_failed", 503);
|
|
2552
2552
|
}
|
|
@@ -2554,7 +2554,7 @@ var handleSignupControl = async (req, url, env, ctx) => {
|
|
|
2554
2554
|
ok: true,
|
|
2555
2555
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
2556
2556
|
revision: document.revision,
|
|
2557
|
-
digest,
|
|
2557
|
+
digest: digest2,
|
|
2558
2558
|
status: "accepted",
|
|
2559
2559
|
acknowledgedAt: receivedAt
|
|
2560
2560
|
});
|
|
@@ -2582,8 +2582,8 @@ function canonicalMemberContent(value) {
|
|
|
2582
2582
|
return canonicalValue2(value);
|
|
2583
2583
|
}
|
|
2584
2584
|
async function memberContentDigest(document) {
|
|
2585
|
-
const
|
|
2586
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
2585
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2586
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2587
2587
|
}
|
|
2588
2588
|
|
|
2589
2589
|
// src/member-content-validation-helpers.ts
|
|
@@ -2620,21 +2620,21 @@ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
|
2620
2620
|
return valueText;
|
|
2621
2621
|
}
|
|
2622
2622
|
function offerValue(value, path, errors, tiers, audience) {
|
|
2623
|
-
const
|
|
2624
|
-
if (!
|
|
2623
|
+
const raw2 = record2(value);
|
|
2624
|
+
if (!raw2) {
|
|
2625
2625
|
errors.push(`${path} must be an object`);
|
|
2626
2626
|
return void 0;
|
|
2627
2627
|
}
|
|
2628
|
-
unknownKeys2(
|
|
2629
|
-
const tierId = textValue(
|
|
2628
|
+
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
2629
|
+
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2630
2630
|
if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2631
2631
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2632
2632
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2633
2633
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
2634
2634
|
}
|
|
2635
|
-
const label = textValue(
|
|
2636
|
-
const bodyMarkdown = textValue(
|
|
2637
|
-
const ctaLabel = textValue(
|
|
2635
|
+
const label = textValue(raw2.label, `${path}.label`, errors);
|
|
2636
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
|
|
2637
|
+
const ctaLabel = textValue(raw2.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2638
2638
|
return {
|
|
2639
2639
|
tierId,
|
|
2640
2640
|
...label ? { label } : {},
|
|
@@ -2686,37 +2686,37 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2686
2686
|
const ids = /* @__PURE__ */ new Set();
|
|
2687
2687
|
const slugs = /* @__PURE__ */ new Set();
|
|
2688
2688
|
const pages = [];
|
|
2689
|
-
for (const [
|
|
2690
|
-
const path = `document.pages[${
|
|
2691
|
-
const
|
|
2692
|
-
if (!
|
|
2689
|
+
for (const [index2, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2690
|
+
const path = `document.pages[${index2}]`;
|
|
2691
|
+
const raw2 = record2(value);
|
|
2692
|
+
if (!raw2) {
|
|
2693
2693
|
errors.push(`${path} must be an object`);
|
|
2694
2694
|
continue;
|
|
2695
2695
|
}
|
|
2696
|
-
unknownKeys2(
|
|
2697
|
-
const id = textValue(
|
|
2698
|
-
const slug = textValue(
|
|
2699
|
-
const title = textValue(
|
|
2696
|
+
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
2697
|
+
const id = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2698
|
+
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2699
|
+
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2700
2700
|
if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2701
2701
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2702
2702
|
if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
|
|
2703
2703
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2704
2704
|
ids.add(id);
|
|
2705
2705
|
slugs.add(slug);
|
|
2706
|
-
const navRaw = record2(
|
|
2707
|
-
let navigation = { section: "General", order:
|
|
2706
|
+
const navRaw = record2(raw2.navigation);
|
|
2707
|
+
let navigation = { section: "General", order: index2 };
|
|
2708
2708
|
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2709
2709
|
else {
|
|
2710
2710
|
unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2711
2711
|
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2712
|
-
const
|
|
2713
|
-
if (!Number.isInteger(
|
|
2712
|
+
const order2 = navRaw.order;
|
|
2713
|
+
if (!Number.isInteger(order2) || Number(order2) < 0 || Number(order2) > 1e6) {
|
|
2714
2714
|
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2715
2715
|
}
|
|
2716
2716
|
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2717
|
-
navigation = { section, order: Number.isInteger(
|
|
2717
|
+
navigation = { section, order: Number.isInteger(order2) ? Number(order2) : index2, ...parentId ? { parentId } : {} };
|
|
2718
2718
|
}
|
|
2719
|
-
const audienceRaw = record2(
|
|
2719
|
+
const audienceRaw = record2(raw2.audience);
|
|
2720
2720
|
let audience = { kind: "all-members" };
|
|
2721
2721
|
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2722
2722
|
else {
|
|
@@ -2738,12 +2738,12 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2738
2738
|
audience = { kind: "tiers", tierIds: normalized };
|
|
2739
2739
|
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2740
2740
|
}
|
|
2741
|
-
const summary = textValue(
|
|
2742
|
-
const teaserMarkdown = textValue(
|
|
2743
|
-
const bodyMarkdown = textValue(
|
|
2741
|
+
const summary = textValue(raw2.summary, `${path}.summary`, errors, 2e3);
|
|
2742
|
+
const teaserMarkdown = textValue(raw2.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
|
|
2743
|
+
const bodyMarkdown = textValue(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
|
|
2744
2744
|
let upsell;
|
|
2745
|
-
if (
|
|
2746
|
-
const upsellRaw = record2(
|
|
2745
|
+
if (raw2.upsell !== void 0) {
|
|
2746
|
+
const upsellRaw = record2(raw2.upsell);
|
|
2747
2747
|
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2748
2748
|
else {
|
|
2749
2749
|
unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
@@ -2760,10 +2760,10 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2760
2760
|
pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2761
2761
|
}
|
|
2762
2762
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2763
|
-
for (const [
|
|
2763
|
+
for (const [index2, page] of pages.entries()) {
|
|
2764
2764
|
const parentId = page.navigation.parentId;
|
|
2765
|
-
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${
|
|
2766
|
-
if (parentId === page.id) errors.push(`document.pages[${
|
|
2765
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index2}].navigation.parentId references unknown page "${parentId}"`);
|
|
2766
|
+
if (parentId === page.id) errors.push(`document.pages[${index2}].navigation.parentId cannot reference itself`);
|
|
2767
2767
|
}
|
|
2768
2768
|
for (const page of pages) {
|
|
2769
2769
|
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
@@ -2784,21 +2784,21 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2784
2784
|
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2785
2785
|
const seen = /* @__PURE__ */ new Set();
|
|
2786
2786
|
upsellOverrides = [];
|
|
2787
|
-
for (const [
|
|
2788
|
-
const path = `document.upsellOverrides[${
|
|
2789
|
-
const
|
|
2790
|
-
if (!
|
|
2787
|
+
for (const [index2, value] of values.entries()) {
|
|
2788
|
+
const path = `document.upsellOverrides[${index2}]`;
|
|
2789
|
+
const raw2 = record2(value);
|
|
2790
|
+
if (!raw2) {
|
|
2791
2791
|
errors.push(`${path} must be an object`);
|
|
2792
2792
|
continue;
|
|
2793
2793
|
}
|
|
2794
|
-
unknownKeys2(
|
|
2795
|
-
const pageId = textValue(
|
|
2794
|
+
unknownKeys2(raw2, OVERRIDE_KEYS, path, errors);
|
|
2795
|
+
const pageId = textValue(raw2.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2796
2796
|
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2797
2797
|
seen.add(pageId);
|
|
2798
2798
|
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2799
2799
|
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2800
2800
|
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2801
|
-
const offer = offerValue(
|
|
2801
|
+
const offer = offerValue(raw2.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2802
2802
|
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2803
2803
|
}
|
|
2804
2804
|
}
|
|
@@ -2876,36 +2876,36 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2876
2876
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2877
2877
|
async function exists(db, ns, where) {
|
|
2878
2878
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2879
|
-
const
|
|
2880
|
-
return
|
|
2879
|
+
const rows6 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2880
|
+
return rows6.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2881
2881
|
}
|
|
2882
2882
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2883
2883
|
const where = { targetId, environment, active: true };
|
|
2884
2884
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2885
|
-
const
|
|
2885
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
2886
2886
|
where,
|
|
2887
2887
|
order: { revision: "desc" },
|
|
2888
2888
|
limit: 1
|
|
2889
2889
|
} } })).memberContentControls;
|
|
2890
|
-
return control(Array.isArray(
|
|
2890
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2891
2891
|
}
|
|
2892
2892
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2893
2893
|
const where = { targetId, environment };
|
|
2894
2894
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2895
|
-
const
|
|
2895
|
+
const rows6 = (await db.query({ memberContentControls: { $: {
|
|
2896
2896
|
where,
|
|
2897
2897
|
order: { revision: "desc" },
|
|
2898
2898
|
limit: 1
|
|
2899
2899
|
} } })).memberContentControls;
|
|
2900
|
-
return control(Array.isArray(
|
|
2900
|
+
return control(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2901
2901
|
}
|
|
2902
2902
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2903
2903
|
const id = `member-content-local:${environment}`;
|
|
2904
|
-
const
|
|
2905
|
-
return local(Array.isArray(
|
|
2904
|
+
const rows6 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2905
|
+
return local(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
2906
2906
|
}
|
|
2907
2907
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2908
|
-
const { document, digest } = envelope;
|
|
2908
|
+
const { document, digest: digest2 } = envelope;
|
|
2909
2909
|
const target = document.target;
|
|
2910
2910
|
const id = `member-content-control:${target.environment}:${document.revision}`;
|
|
2911
2911
|
const ops = [];
|
|
@@ -2918,12 +2918,12 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
2918
2918
|
targetId: target.chapterId,
|
|
2919
2919
|
environment: target.environment,
|
|
2920
2920
|
revision: document.revision,
|
|
2921
|
-
digest,
|
|
2921
|
+
digest: digest2,
|
|
2922
2922
|
document,
|
|
2923
2923
|
active: true,
|
|
2924
2924
|
receivedAt
|
|
2925
2925
|
} });
|
|
2926
|
-
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${
|
|
2926
|
+
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest2}` });
|
|
2927
2927
|
}
|
|
2928
2928
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
2929
2929
|
const id = `member-content-local:${environment}`;
|
|
@@ -2957,12 +2957,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2957
2957
|
}
|
|
2958
2958
|
async function memberTierId(db, chapterId, email) {
|
|
2959
2959
|
if (!email) return null;
|
|
2960
|
-
let
|
|
2960
|
+
let rows6 = (await db.query({ applications: { $: {
|
|
2961
2961
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2962
2962
|
limit: 20
|
|
2963
2963
|
} } })).applications;
|
|
2964
|
-
if (!
|
|
2965
|
-
const latest = Array.isArray(
|
|
2964
|
+
if (!rows6?.length) rows6 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2965
|
+
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;
|
|
2966
2966
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2967
2967
|
}
|
|
2968
2968
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -2987,7 +2987,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
2987
2987
|
} catch {
|
|
2988
2988
|
return reject2("invalid_document", 400, ["request body must be JSON"]);
|
|
2989
2989
|
}
|
|
2990
|
-
const
|
|
2990
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
2991
2991
|
const initial = validateMemberContentDocument(input.document, {
|
|
2992
2992
|
sourceType: "parent"
|
|
2993
2993
|
});
|
|
@@ -3015,11 +3015,11 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3015
3015
|
return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
|
|
3016
3016
|
}
|
|
3017
3017
|
}
|
|
3018
|
-
if (!
|
|
3018
|
+
if (!digest2 || await memberContentDigest(document) !== digest2) return reject2("invalid_digest", 400);
|
|
3019
3019
|
const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
3020
3020
|
if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
|
|
3021
3021
|
if (latest && document.revision === latest.revision) {
|
|
3022
|
-
if (latest.digest !==
|
|
3022
|
+
if (latest.digest !== digest2) return reject2("conflicting_revision", 409);
|
|
3023
3023
|
return json({
|
|
3024
3024
|
ok: true,
|
|
3025
3025
|
target: document.target,
|
|
@@ -3031,7 +3031,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3031
3031
|
}
|
|
3032
3032
|
const receivedAt = Date.now();
|
|
3033
3033
|
try {
|
|
3034
|
-
await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
|
|
3034
|
+
await materializeMemberContentControl(db, { digest: digest2, document }, latest, receivedAt);
|
|
3035
3035
|
} catch {
|
|
3036
3036
|
return reject2("materialization_failed", 503);
|
|
3037
3037
|
}
|
|
@@ -3039,7 +3039,7 @@ var handleMemberContentControl = async (req, url, env, ctx) => {
|
|
|
3039
3039
|
ok: true,
|
|
3040
3040
|
target: document.target,
|
|
3041
3041
|
revision: document.revision,
|
|
3042
|
-
digest,
|
|
3042
|
+
digest: digest2,
|
|
3043
3043
|
status: "accepted",
|
|
3044
3044
|
acknowledgedAt: receivedAt
|
|
3045
3045
|
});
|
|
@@ -3091,11 +3091,11 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
3091
3091
|
}
|
|
3092
3092
|
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
3093
3093
|
if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
3094
|
-
const
|
|
3095
|
-
if (
|
|
3094
|
+
const raw2 = await req.text();
|
|
3095
|
+
if (raw2.length > BODY_CAP) return json({ error: "request body too large" }, 413);
|
|
3096
3096
|
let input;
|
|
3097
3097
|
try {
|
|
3098
|
-
input = JSON.parse(
|
|
3098
|
+
input = JSON.parse(raw2);
|
|
3099
3099
|
} catch {
|
|
3100
3100
|
return json({ error: "invalid JSON body" }, 400);
|
|
3101
3101
|
}
|
|
@@ -3163,18 +3163,18 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3163
3163
|
} catch {
|
|
3164
3164
|
return reject3("invalid_document", 400, ["request body must be JSON"]);
|
|
3165
3165
|
}
|
|
3166
|
-
const
|
|
3166
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
3167
3167
|
const checked = validateAdmissionGrantDocument(input.document);
|
|
3168
3168
|
if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
|
|
3169
3169
|
const document = checked.value;
|
|
3170
3170
|
if (document.source.appId !== claimedSender) return reject3("wrong_source", 403);
|
|
3171
3171
|
if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
|
|
3172
3172
|
if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
|
|
3173
|
-
if (!
|
|
3173
|
+
if (!digest2 || await signupControlDigest(document) !== digest2) return reject3("invalid_digest", 400);
|
|
3174
3174
|
const latest = await loadLatestAdmissionRevision(db, claimedSender, ctx.chapter.id, env.ODLA_ENV);
|
|
3175
3175
|
if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
|
|
3176
3176
|
if (latest && document.revision === latest.revision) {
|
|
3177
|
-
if (latest.digest !==
|
|
3177
|
+
if (latest.digest !== digest2) return reject3("conflicting_revision", 409);
|
|
3178
3178
|
return json({
|
|
3179
3179
|
ok: true,
|
|
3180
3180
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
@@ -3186,7 +3186,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3186
3186
|
}
|
|
3187
3187
|
const receivedAt = Date.now();
|
|
3188
3188
|
try {
|
|
3189
|
-
await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
|
|
3189
|
+
await materializeAdmissionGrants(db, { digest: digest2, document }, latest, receivedAt);
|
|
3190
3190
|
} catch {
|
|
3191
3191
|
return reject3("materialization_failed", 503);
|
|
3192
3192
|
}
|
|
@@ -3194,7 +3194,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3194
3194
|
ok: true,
|
|
3195
3195
|
target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
|
|
3196
3196
|
revision: document.revision,
|
|
3197
|
-
digest,
|
|
3197
|
+
digest: digest2,
|
|
3198
3198
|
status: "accepted",
|
|
3199
3199
|
acknowledgedAt: receivedAt
|
|
3200
3200
|
});
|
|
@@ -3397,9 +3397,9 @@ function personAdmissionEmail(ctx, record4) {
|
|
|
3397
3397
|
if (record4.type !== "person") return null;
|
|
3398
3398
|
const field = ctx.chapter.crm.config.types.person?.emailField;
|
|
3399
3399
|
if (!field) return null;
|
|
3400
|
-
const
|
|
3401
|
-
if (typeof
|
|
3402
|
-
const normalized = normalizeAdmissionEmail(
|
|
3400
|
+
const raw2 = record4.fields[field] ?? record4.primaryEmail;
|
|
3401
|
+
if (typeof raw2 !== "string") return null;
|
|
3402
|
+
const normalized = normalizeAdmissionEmail(raw2);
|
|
3403
3403
|
return isValidEmail(normalized) ? normalized : null;
|
|
3404
3404
|
}
|
|
3405
3405
|
async function grantInput(db, ctx, assignment) {
|
|
@@ -3419,7 +3419,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3419
3419
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
3420
3420
|
const id = assignmentId(opts.target.id, opts.personId);
|
|
3421
3421
|
const existing = current.find((row) => row.id === id);
|
|
3422
|
-
const
|
|
3422
|
+
const digest2 = await signupControlDigest({
|
|
3423
3423
|
targetId: opts.target.id,
|
|
3424
3424
|
personId: opts.personId,
|
|
3425
3425
|
decision: opts.decision,
|
|
@@ -3434,7 +3434,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3434
3434
|
} } });
|
|
3435
3435
|
const receipt = admissionRows(priorMutation2.leaderAdmissionMutations)[0];
|
|
3436
3436
|
if (receipt) {
|
|
3437
|
-
if (receipt.requestDigest !==
|
|
3437
|
+
if (receipt.requestDigest !== digest2 || receipt.personId !== opts.personId) {
|
|
3438
3438
|
throw new Error("mutation_conflict");
|
|
3439
3439
|
}
|
|
3440
3440
|
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
@@ -3442,7 +3442,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3442
3442
|
return { delivery: saved, duplicate: true };
|
|
3443
3443
|
}
|
|
3444
3444
|
if (existing?.lastMutationId === opts.mutationId) {
|
|
3445
|
-
if (existing.lastRequestDigest !==
|
|
3445
|
+
if (existing.lastRequestDigest !== digest2) throw new Error("mutation_conflict");
|
|
3446
3446
|
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3447
3447
|
if (!saved) throw new Error("delivery_missing");
|
|
3448
3448
|
return { delivery: saved, duplicate: true };
|
|
@@ -3457,7 +3457,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3457
3457
|
...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {},
|
|
3458
3458
|
active: opts.active,
|
|
3459
3459
|
lastMutationId: opts.mutationId,
|
|
3460
|
-
lastRequestDigest:
|
|
3460
|
+
lastRequestDigest: digest2,
|
|
3461
3461
|
createdAt: existing?.createdAt ?? now,
|
|
3462
3462
|
createdBy: existing?.createdBy ?? opts.actor,
|
|
3463
3463
|
updatedAt: now,
|
|
@@ -3493,7 +3493,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
3493
3493
|
id: mutationReceiptId,
|
|
3494
3494
|
targetId: opts.target.id,
|
|
3495
3495
|
personId: opts.personId,
|
|
3496
|
-
requestDigest:
|
|
3496
|
+
requestDigest: digest2,
|
|
3497
3497
|
revision,
|
|
3498
3498
|
createdAt: now
|
|
3499
3499
|
} }
|
|
@@ -3688,8 +3688,8 @@ function makeCalendar(env) {
|
|
|
3688
3688
|
}
|
|
3689
3689
|
async function firstRow(db, ns, q) {
|
|
3690
3690
|
const res = await db.query({ [ns]: { $: q } });
|
|
3691
|
-
const
|
|
3692
|
-
return Array.isArray(
|
|
3691
|
+
const rows6 = res[ns];
|
|
3692
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
3693
3693
|
}
|
|
3694
3694
|
async function computeSlots(cal, cfg) {
|
|
3695
3695
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -3946,17 +3946,17 @@ async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
|
3946
3946
|
try {
|
|
3947
3947
|
const group = await groupFor(db, app);
|
|
3948
3948
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
3949
|
-
const
|
|
3949
|
+
const text5 = (value) => typeof value === "string" ? value : "";
|
|
3950
3950
|
await sendTemplated(deps(db, env), {
|
|
3951
3951
|
group: emailGroupFrom(group),
|
|
3952
3952
|
template: "adminNotification",
|
|
3953
3953
|
to: group.notificationEmail,
|
|
3954
3954
|
vars: {
|
|
3955
|
-
firstName:
|
|
3956
|
-
lastName:
|
|
3957
|
-
email:
|
|
3958
|
-
phone:
|
|
3959
|
-
state:
|
|
3955
|
+
firstName: text5(app.firstName),
|
|
3956
|
+
lastName: text5(app.lastName),
|
|
3957
|
+
email: text5(app.email),
|
|
3958
|
+
phone: text5(app.phone),
|
|
3959
|
+
state: text5(app.state)
|
|
3960
3960
|
},
|
|
3961
3961
|
dedupeKey: `${eventId}:admin`,
|
|
3962
3962
|
applicationId: String(app.id)
|
|
@@ -4089,8 +4089,8 @@ function createStripeProvider(config) {
|
|
|
4089
4089
|
const sk = requireSecret(secretKey);
|
|
4090
4090
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4091
4091
|
if (!charges.ok) fail("charges list", charges);
|
|
4092
|
-
const
|
|
4093
|
-
const paid =
|
|
4092
|
+
const rows6 = charges.body.data ?? [];
|
|
4093
|
+
const paid = rows6.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4094
4094
|
const charge = paid[paid.length - 1];
|
|
4095
4095
|
if (!charge) {
|
|
4096
4096
|
const err = new Error("no paid charge to refund");
|
|
@@ -4198,8 +4198,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4198
4198
|
throw new Error("gift issuance conflict");
|
|
4199
4199
|
}
|
|
4200
4200
|
}
|
|
4201
|
-
function buyerGiftClaims(
|
|
4202
|
-
return
|
|
4201
|
+
function buyerGiftClaims(rows6, now) {
|
|
4202
|
+
return rows6.map((row) => {
|
|
4203
4203
|
const stored = String(row.status ?? "pending");
|
|
4204
4204
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4205
4205
|
return {
|
|
@@ -4491,8 +4491,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4491
4491
|
|
|
4492
4492
|
// src/stripe-webhook-partition.ts
|
|
4493
4493
|
var firstRow2 = async (db, ns, q) => {
|
|
4494
|
-
const
|
|
4495
|
-
return Array.isArray(
|
|
4494
|
+
const rows6 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4495
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
4496
4496
|
};
|
|
4497
4497
|
var stripeEventDigest = async (rawBody) => {
|
|
4498
4498
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4539,9 +4539,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
4539
4539
|
const raced = await receiptFor(db, input.eventId);
|
|
4540
4540
|
return raced?.digest === input.digest ? "replayed" : "collision";
|
|
4541
4541
|
}
|
|
4542
|
-
async function quarantineStripeEvent(db, eventId,
|
|
4542
|
+
async function quarantineStripeEvent(db, eventId, digest2, event) {
|
|
4543
4543
|
const disposition = "quarantine_unmatched";
|
|
4544
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
4544
|
+
const stored = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition, matched: false });
|
|
4545
4545
|
if (stored === "collision") {
|
|
4546
4546
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4547
4547
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4549,10 +4549,10 @@ async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
|
4549
4549
|
reportStripePartition("warn", eventId, event, disposition);
|
|
4550
4550
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
4551
4551
|
}
|
|
4552
|
-
async function replayedStripeReceipt(db, eventId,
|
|
4552
|
+
async function replayedStripeReceipt(db, eventId, digest2, event) {
|
|
4553
4553
|
const existing = await receiptFor(db, eventId);
|
|
4554
4554
|
if (!existing) return null;
|
|
4555
|
-
if (existing.digest !==
|
|
4555
|
+
if (existing.digest !== digest2) {
|
|
4556
4556
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4557
4557
|
return json({ ok: true, matched: false, collision: true });
|
|
4558
4558
|
}
|
|
@@ -4562,8 +4562,8 @@ async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
|
4562
4562
|
// src/worker-routes-payments.ts
|
|
4563
4563
|
async function firstRow3(db, ns, q) {
|
|
4564
4564
|
const res = await db.query({ [ns]: { $: q } });
|
|
4565
|
-
const
|
|
4566
|
-
return Array.isArray(
|
|
4565
|
+
const rows6 = res[ns];
|
|
4566
|
+
return Array.isArray(rows6) ? rows6[0] : void 0;
|
|
4567
4567
|
}
|
|
4568
4568
|
async function findApplication(db, event) {
|
|
4569
4569
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -4687,7 +4687,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4687
4687
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
4688
4688
|
const { eventId, event } = ingest;
|
|
4689
4689
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4690
|
-
const
|
|
4690
|
+
const digest2 = await stripeEventDigest(rawBody);
|
|
4691
4691
|
const identity = stripeEventIdentity(event);
|
|
4692
4692
|
const target = stripeEventTarget(env, ctx);
|
|
4693
4693
|
const preliminary = classifyMemberStripeEvent(identity, target, false);
|
|
@@ -4701,23 +4701,23 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4701
4701
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4702
4702
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4703
4703
|
}
|
|
4704
|
-
const replay2 = await replayedStripeReceipt(db, eventId,
|
|
4704
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest2, event);
|
|
4705
4705
|
if (replay2) return replay2;
|
|
4706
4706
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
4707
4707
|
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
4708
4708
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
4709
|
-
return quarantineStripeEvent(db, eventId,
|
|
4709
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4710
4710
|
}
|
|
4711
4711
|
if (event.kind === "gift_paid") {
|
|
4712
4712
|
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
4713
|
-
return quarantineStripeEvent(db, eventId,
|
|
4713
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4714
4714
|
}
|
|
4715
4715
|
try {
|
|
4716
4716
|
const issued = await issueGiftClaim(db, intent, {
|
|
4717
4717
|
eventId,
|
|
4718
4718
|
objectId: event.providerObjectId
|
|
4719
4719
|
}, Date.now());
|
|
4720
|
-
const stored3 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: true });
|
|
4720
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition: disposition2, matched: true });
|
|
4721
4721
|
if (stored3 === "collision") {
|
|
4722
4722
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4723
4723
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4733,7 +4733,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4733
4733
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
4734
4734
|
const claim = await firstRow3(db, "giftClaims", { where: { intentId: event.giftIntentId }, limit: 1 });
|
|
4735
4735
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
4736
|
-
const stored2 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4736
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4737
4737
|
if (stored2 === "collision") {
|
|
4738
4738
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4739
4739
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -4745,12 +4745,12 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4745
4745
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4746
4746
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4747
4747
|
}
|
|
4748
|
-
const replay = await replayedStripeReceipt(db, eventId,
|
|
4748
|
+
const replay = await replayedStripeReceipt(db, eventId, digest2, event);
|
|
4749
4749
|
if (replay) return replay;
|
|
4750
4750
|
const localApplication = Boolean(app && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
4751
4751
|
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
4752
4752
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
4753
|
-
return quarantineStripeEvent(db, eventId,
|
|
4753
|
+
return quarantineStripeEvent(db, eventId, digest2, event);
|
|
4754
4754
|
}
|
|
4755
4755
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
4756
4756
|
let next = app;
|
|
@@ -4773,7 +4773,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4773
4773
|
});
|
|
4774
4774
|
}
|
|
4775
4775
|
}
|
|
4776
|
-
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: true });
|
|
4776
|
+
const stored = await storeStripeReceipt(db, { eventId, digest: digest2, event, disposition, matched: true });
|
|
4777
4777
|
if (stored === "collision") {
|
|
4778
4778
|
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4779
4779
|
return json({ ok: true, matched: false, collision: true });
|
|
@@ -5029,8 +5029,8 @@ async function upcomingEvents(env) {
|
|
|
5029
5029
|
const res = await cal.availability.upcoming();
|
|
5030
5030
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5031
5031
|
}
|
|
5032
|
-
function toReconcile(
|
|
5033
|
-
return
|
|
5032
|
+
function toReconcile(rows6) {
|
|
5033
|
+
return rows6.map((m) => ({
|
|
5034
5034
|
id: String(m.id),
|
|
5035
5035
|
applicationId: String(m.applicationId),
|
|
5036
5036
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5052,12 +5052,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5052
5052
|
const query = await db.query({
|
|
5053
5053
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5054
5054
|
});
|
|
5055
|
-
let
|
|
5056
|
-
if (Number.isFinite(from))
|
|
5057
|
-
if (Number.isFinite(to))
|
|
5055
|
+
let rows6 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5056
|
+
if (Number.isFinite(from)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5057
|
+
if (Number.isFinite(to)) rows6 = rows6.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5058
5058
|
let decisions = [];
|
|
5059
5059
|
try {
|
|
5060
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5060
|
+
decisions = reconcileMeetings(toReconcile(rows6), await upcomingEvents(env), Date.now());
|
|
5061
5061
|
} catch {
|
|
5062
5062
|
}
|
|
5063
5063
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5084,7 +5084,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5084
5084
|
};
|
|
5085
5085
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5086
5086
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5087
|
-
const meetings =
|
|
5087
|
+
const meetings = rows6.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5088
5088
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5089
5089
|
};
|
|
5090
5090
|
|
|
@@ -5105,16 +5105,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
5105
5105
|
});
|
|
5106
5106
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
5107
5107
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
5108
|
-
const
|
|
5109
|
-
if (
|
|
5110
|
-
const result = await backfillCrm(crmDeps2(
|
|
5108
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5109
|
+
if (gate12 instanceof Response) return gate12;
|
|
5110
|
+
const result = await backfillCrm(crmDeps2(gate12.db, ctx));
|
|
5111
5111
|
return json({ ok: true, ...result });
|
|
5112
5112
|
};
|
|
5113
5113
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
5114
5114
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
5115
|
-
const
|
|
5116
|
-
if (
|
|
5117
|
-
const { db } =
|
|
5115
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5116
|
+
if (gate12 instanceof Response) return gate12;
|
|
5117
|
+
const { db } = gate12;
|
|
5118
5118
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
5119
5119
|
const [data, roleList] = await Promise.all([
|
|
5120
5120
|
db.query({
|
|
@@ -5160,16 +5160,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5160
5160
|
});
|
|
5161
5161
|
}
|
|
5162
5162
|
}
|
|
5163
|
-
const
|
|
5163
|
+
const rows6 = [...people.values()].sort(
|
|
5164
5164
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5165
5165
|
);
|
|
5166
|
-
return json({ people:
|
|
5166
|
+
return json({ people: rows6 });
|
|
5167
5167
|
};
|
|
5168
5168
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5169
5169
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
5170
|
-
const
|
|
5171
|
-
if (
|
|
5172
|
-
const { db } =
|
|
5170
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5171
|
+
if (gate12 instanceof Response) return gate12;
|
|
5172
|
+
const { db } = gate12;
|
|
5173
5173
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
5174
5174
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
5175
5175
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -5180,9 +5180,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
5180
5180
|
};
|
|
5181
5181
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
5182
5182
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
5183
|
-
const
|
|
5184
|
-
if (
|
|
5185
|
-
const { db, actor } =
|
|
5183
|
+
const gate12 = await adminGate(req, env, ctx);
|
|
5184
|
+
if (gate12 instanceof Response) return gate12;
|
|
5185
|
+
const { db, actor } = gate12;
|
|
5186
5186
|
let body;
|
|
5187
5187
|
try {
|
|
5188
5188
|
body = await req.json();
|
|
@@ -5218,8 +5218,8 @@ function canonical(value) {
|
|
|
5218
5218
|
}
|
|
5219
5219
|
return JSON.stringify(value) ?? "undefined";
|
|
5220
5220
|
}
|
|
5221
|
-
function positiveInt(
|
|
5222
|
-
const parsed = Number(
|
|
5221
|
+
function positiveInt(raw2, fallback, max) {
|
|
5222
|
+
const parsed = Number(raw2);
|
|
5223
5223
|
return Number.isSafeInteger(parsed) && parsed >= 0 ? Math.min(parsed, max) : fallback;
|
|
5224
5224
|
}
|
|
5225
5225
|
var handleAdminClerkPrivateProfileSync = async (req, url, env, ctx) => {
|
|
@@ -5300,14 +5300,14 @@ function periodSeries(points, now, count, periodMs) {
|
|
|
5300
5300
|
for (const point of points) {
|
|
5301
5301
|
if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
|
|
5302
5302
|
if (point.t >= start && point.t <= now) {
|
|
5303
|
-
const
|
|
5304
|
-
current[
|
|
5303
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
|
|
5304
|
+
current[index2] = (current[index2] ?? 0) + point.v;
|
|
5305
5305
|
} else if (point.t >= previousStart && point.t < start) {
|
|
5306
|
-
const
|
|
5307
|
-
previous[
|
|
5306
|
+
const index2 = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
|
|
5307
|
+
previous[index2] = (previous[index2] ?? 0) + point.v;
|
|
5308
5308
|
}
|
|
5309
5309
|
}
|
|
5310
|
-
const labels = current.map((_,
|
|
5310
|
+
const labels = current.map((_, index2) => new Date(start + index2 * periodMs).toISOString().slice(5, 10));
|
|
5311
5311
|
return { labels, current, previous };
|
|
5312
5312
|
}
|
|
5313
5313
|
function dashboardMetricData(points, now) {
|
|
@@ -5720,11 +5720,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
5720
5720
|
for (const key of EMAIL_TEMPLATE_NAMES) {
|
|
5721
5721
|
const t = templates[key];
|
|
5722
5722
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
5723
|
-
const
|
|
5724
|
-
if (!subject || !
|
|
5723
|
+
const text5 = typeof t?.text === "string" ? t.text : "";
|
|
5724
|
+
if (!subject || !text5.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
|
|
5725
5725
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
|
|
5726
|
-
if (
|
|
5727
|
-
clean[key] = { subject, text:
|
|
5726
|
+
if (text5.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
|
|
5727
|
+
clean[key] = { subject, text: text5, enabled: t?.enabled !== false };
|
|
5728
5728
|
}
|
|
5729
5729
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5730
5730
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -6751,6 +6751,1192 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6751
6751
|
}
|
|
6752
6752
|
};
|
|
6753
6753
|
|
|
6754
|
+
// src/network-person-registry.ts
|
|
6755
|
+
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6756
|
+
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6757
|
+
function normalizeNetworkIdentity(kind, value) {
|
|
6758
|
+
const trimmed = value.trim();
|
|
6759
|
+
if (kind === "email") return trimmed.toLowerCase();
|
|
6760
|
+
if (kind === "phone") return trimmed.replace(/[\s().-]/g, "");
|
|
6761
|
+
return trimmed;
|
|
6762
|
+
}
|
|
6763
|
+
function emailLooksReal(value) {
|
|
6764
|
+
const at = value.lastIndexOf("@");
|
|
6765
|
+
if (at <= 0 || at === value.length - 1) return false;
|
|
6766
|
+
const domain = value.slice(at + 1).toLowerCase();
|
|
6767
|
+
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
6768
|
+
}
|
|
6769
|
+
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
6770
|
+
const errors = [];
|
|
6771
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, errors: ["import must be an object"] };
|
|
6772
|
+
const raw2 = input;
|
|
6773
|
+
if (raw2.sourceKind !== "external_reimport") errors.push("sourceKind must be external_reimport");
|
|
6774
|
+
if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
6775
|
+
const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
|
|
6776
|
+
const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
|
|
6777
|
+
const provenance = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
|
|
6778
|
+
const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
|
|
6779
|
+
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
6780
|
+
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6781
|
+
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6782
|
+
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6783
|
+
const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
|
|
6784
|
+
const consentStates = ["unknown", "granted", "denied"];
|
|
6785
|
+
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
6786
|
+
const discretionary = typeof rawConsent.discretionary === "string" ? rawConsent.discretionary : "";
|
|
6787
|
+
const recordedAt = Number(rawConsent.recordedAt);
|
|
6788
|
+
const basis = typeof rawConsent.basis === "string" ? rawConsent.basis.trim() : "";
|
|
6789
|
+
if (!consentStates.includes(marketing) || !consentStates.includes(discretionary) || !Number.isSafeInteger(recordedAt) || recordedAt <= 0 || !basis || basis.length > 500) {
|
|
6790
|
+
errors.push("consent must include marketing and discretionary states, recordedAt, and a bounded basis");
|
|
6791
|
+
}
|
|
6792
|
+
const identities = [];
|
|
6793
|
+
if (!Array.isArray(raw2.identities) || raw2.identities.length < 1 || raw2.identities.length > 8) {
|
|
6794
|
+
errors.push("one to eight verified identities are required");
|
|
6795
|
+
} else {
|
|
6796
|
+
for (const item of raw2.identities) {
|
|
6797
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6798
|
+
errors.push("identity must be an object");
|
|
6799
|
+
continue;
|
|
6800
|
+
}
|
|
6801
|
+
const candidate = item;
|
|
6802
|
+
const kind = candidate.kind;
|
|
6803
|
+
const value = typeof candidate.value === "string" ? candidate.value : "";
|
|
6804
|
+
const verifiedAt = Number(candidate.verifiedAt);
|
|
6805
|
+
if (kind !== "email" && kind !== "phone" && kind !== "external") {
|
|
6806
|
+
errors.push("identity kind is invalid");
|
|
6807
|
+
continue;
|
|
6808
|
+
}
|
|
6809
|
+
const normalized = normalizeNetworkIdentity(kind, value);
|
|
6810
|
+
if (!normalized || normalized.length > 320 || !Number.isSafeInteger(verifiedAt) || verifiedAt <= 0) {
|
|
6811
|
+
errors.push("identity value and verifiedAt are required and bounded");
|
|
6812
|
+
continue;
|
|
6813
|
+
}
|
|
6814
|
+
if (kind === "email" && !emailLooksReal(normalized)) {
|
|
6815
|
+
errors.push("development or invalid email identities cannot become Persons");
|
|
6816
|
+
continue;
|
|
6817
|
+
}
|
|
6818
|
+
identities.push({ kind, value: normalized, verifiedAt });
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6821
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6822
|
+
for (const identity of identities) {
|
|
6823
|
+
const key = `${identity.kind}:${identity.value}`;
|
|
6824
|
+
if (seen.has(key)) errors.push("duplicate identity in import");
|
|
6825
|
+
seen.add(key);
|
|
6826
|
+
}
|
|
6827
|
+
const chapterLinks = [];
|
|
6828
|
+
if (raw2.chapterLinks !== void 0) {
|
|
6829
|
+
if (!Array.isArray(raw2.chapterLinks) || raw2.chapterLinks.length > allowedChapterIds.length) errors.push("chapterLinks are invalid or too numerous");
|
|
6830
|
+
else for (const item of raw2.chapterLinks) {
|
|
6831
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6832
|
+
errors.push("chapter link must be an object");
|
|
6833
|
+
continue;
|
|
6834
|
+
}
|
|
6835
|
+
const chapterId = typeof item.chapterId === "string" ? String(item.chapterId) : "";
|
|
6836
|
+
const chapterPersonId = typeof item.chapterPersonId === "string" ? String(item.chapterPersonId).trim() : "";
|
|
6837
|
+
if (!CHAPTER_ID.test(chapterId) || !allowedChapterIds.includes(chapterId) || !chapterPersonId || chapterPersonId.length > 200) errors.push("chapter link is outside the configured network");
|
|
6838
|
+
else chapterLinks.push({ chapterId, chapterPersonId });
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
if (errors.length) return { ok: false, errors };
|
|
6842
|
+
return { ok: true, errors: [], value: {
|
|
6843
|
+
sourceKind: "external_reimport",
|
|
6844
|
+
sourceSystem,
|
|
6845
|
+
sourceRecordId,
|
|
6846
|
+
provenance,
|
|
6847
|
+
approved: true,
|
|
6848
|
+
isRealPerson: true,
|
|
6849
|
+
displayName,
|
|
6850
|
+
consent: {
|
|
6851
|
+
marketing,
|
|
6852
|
+
discretionary,
|
|
6853
|
+
recordedAt,
|
|
6854
|
+
basis
|
|
6855
|
+
},
|
|
6856
|
+
identities,
|
|
6857
|
+
...chapterLinks.length ? { chapterLinks } : {}
|
|
6858
|
+
} };
|
|
6859
|
+
}
|
|
6860
|
+
async function sha256(value) {
|
|
6861
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
6862
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
6863
|
+
}
|
|
6864
|
+
async function networkIdentityClaim(input) {
|
|
6865
|
+
const normalized = normalizeNetworkIdentity(input.kind, input.value);
|
|
6866
|
+
const digest2 = await sha256(`${input.kind}\0${normalized}`);
|
|
6867
|
+
const hint = input.kind === "email" ? `${normalized.slice(0, 1)}***@${normalized.split("@")[1]}` : input.kind === "phone" ? `***${normalized.slice(-4)}` : void 0;
|
|
6868
|
+
return { id: `identity_${digest2}`, kind: input.kind, value: normalized, ...hint ? { hint } : {}, verifiedAt: input.verifiedAt };
|
|
6869
|
+
}
|
|
6870
|
+
async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
6871
|
+
return `alias_${await sha256(`${sourceSystem}\0${sourceRecordId}`)}`;
|
|
6872
|
+
}
|
|
6873
|
+
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6874
|
+
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6875
|
+
}
|
|
6876
|
+
function projectNetworkContactPolicy(policy, links) {
|
|
6877
|
+
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6878
|
+
}
|
|
6879
|
+
function canonicalNetworkPersonId(personId, persons) {
|
|
6880
|
+
const byId = new Map(persons.map((person) => [person.id, person]));
|
|
6881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6882
|
+
let current = personId;
|
|
6883
|
+
while (true) {
|
|
6884
|
+
if (seen.has(current)) return null;
|
|
6885
|
+
seen.add(current);
|
|
6886
|
+
const person = byId.get(current);
|
|
6887
|
+
if (!person) return null;
|
|
6888
|
+
if (!person.mergedIntoId) return person.id;
|
|
6889
|
+
current = person.mergedIntoId;
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
// src/network-person-store.ts
|
|
6894
|
+
var NetworkPersonRegistryError = class extends Error {
|
|
6895
|
+
constructor(code, message) {
|
|
6896
|
+
super(message);
|
|
6897
|
+
this.code = code;
|
|
6898
|
+
}
|
|
6899
|
+
code;
|
|
6900
|
+
};
|
|
6901
|
+
var rows4 = (value) => Array.isArray(value) ? value : [];
|
|
6902
|
+
async function loadNetworkPersonRegistry(db) {
|
|
6903
|
+
const result = await db.query({
|
|
6904
|
+
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6905
|
+
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
6906
|
+
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6907
|
+
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
6908
|
+
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
6909
|
+
});
|
|
6910
|
+
return {
|
|
6911
|
+
persons: rows4(result.networkPersons),
|
|
6912
|
+
claims: rows4(result.networkIdentityClaims),
|
|
6913
|
+
aliases: rows4(result.networkPersonAliases),
|
|
6914
|
+
links: rows4(result.networkChapterPersonLinks),
|
|
6915
|
+
events: rows4(result.networkPersonEvents)
|
|
6916
|
+
};
|
|
6917
|
+
}
|
|
6918
|
+
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
6919
|
+
t: "update",
|
|
6920
|
+
ns: "networkPersonEvents",
|
|
6921
|
+
id: `network_person_event:${mutationId}`,
|
|
6922
|
+
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
6923
|
+
});
|
|
6924
|
+
var defaultPolicy = (now, consent) => ({
|
|
6925
|
+
marketingSuppressed: consent.marketing !== "granted",
|
|
6926
|
+
discretionarySuppressed: consent.discretionary !== "granted",
|
|
6927
|
+
updatedAt: now
|
|
6928
|
+
});
|
|
6929
|
+
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
6930
|
+
async function linkOps(personId, links, now) {
|
|
6931
|
+
return Promise.all(links.map(async (link) => {
|
|
6932
|
+
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
6933
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, state: "shadow", createdAt: now } };
|
|
6934
|
+
}));
|
|
6935
|
+
}
|
|
6936
|
+
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
6937
|
+
const alias = {
|
|
6938
|
+
id: aliasId,
|
|
6939
|
+
sourceSystem: input.sourceSystem,
|
|
6940
|
+
sourceRecordId: input.sourceRecordId,
|
|
6941
|
+
sourceKey: aliasId,
|
|
6942
|
+
displayName: input.displayName,
|
|
6943
|
+
provenance: input.provenance,
|
|
6944
|
+
consent: input.consent,
|
|
6945
|
+
status: "ambiguous",
|
|
6946
|
+
candidatePersonIds: candidates,
|
|
6947
|
+
identityClaims: claims,
|
|
6948
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6949
|
+
revision: 1,
|
|
6950
|
+
createdAt: now,
|
|
6951
|
+
updatedAt: now
|
|
6952
|
+
};
|
|
6953
|
+
await db.transact([
|
|
6954
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
6955
|
+
eventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
6956
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
6957
|
+
return { status: "ambiguous", alias };
|
|
6958
|
+
}
|
|
6959
|
+
async function importNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
6960
|
+
const aliasId = await networkSourceKey(input.sourceSystem, input.sourceRecordId);
|
|
6961
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
6962
|
+
const replay = state.aliases.find((alias2) => alias2.id === aliasId);
|
|
6963
|
+
if (replay) return { status: replay.status, alias: replay, replayed: true };
|
|
6964
|
+
const claims = await Promise.all(input.identities.map(networkIdentityClaim));
|
|
6965
|
+
const candidates = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
6966
|
+
if (candidates.length) return ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now);
|
|
6967
|
+
const personId = `person_${aliasId.slice("alias_".length)}`;
|
|
6968
|
+
const person = { id: personId, displayName: input.displayName, state: "active", contactPolicy: defaultPolicy(now, input.consent), revision: 1, createdAt: now, updatedAt: now };
|
|
6969
|
+
const alias = {
|
|
6970
|
+
id: aliasId,
|
|
6971
|
+
sourceSystem: input.sourceSystem,
|
|
6972
|
+
sourceRecordId: input.sourceRecordId,
|
|
6973
|
+
sourceKey: aliasId,
|
|
6974
|
+
displayName: input.displayName,
|
|
6975
|
+
provenance: input.provenance,
|
|
6976
|
+
consent: input.consent,
|
|
6977
|
+
status: "linked",
|
|
6978
|
+
personId,
|
|
6979
|
+
identityClaims: claims,
|
|
6980
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6981
|
+
revision: 1,
|
|
6982
|
+
createdAt: now,
|
|
6983
|
+
updatedAt: now
|
|
6984
|
+
};
|
|
6985
|
+
const claimOps = claims.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId, createdAt: now } }));
|
|
6986
|
+
const ops = [
|
|
6987
|
+
{ t: "update", ns: "networkPersons", id: personId, attrs: { ...person } },
|
|
6988
|
+
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
6989
|
+
...claimOps,
|
|
6990
|
+
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
6991
|
+
eventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
6992
|
+
];
|
|
6993
|
+
try {
|
|
6994
|
+
await db.transact(ops, { mutationId, guards: [
|
|
6995
|
+
{ ns: "networkPersonAliases", id: aliasId, exists: false },
|
|
6996
|
+
...claims.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
6997
|
+
] });
|
|
6998
|
+
return { status: "linked", alias, person };
|
|
6999
|
+
} catch (error) {
|
|
7000
|
+
if (!isGuardConflict(error)) throw error;
|
|
7001
|
+
state = await loadNetworkPersonRegistry(db);
|
|
7002
|
+
const existing = state.aliases.find((item) => item.id === aliasId);
|
|
7003
|
+
if (existing) return { status: existing.status, alias: existing, replayed: true };
|
|
7004
|
+
const raced = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
7005
|
+
if (!raced.length) throw error;
|
|
7006
|
+
return ambiguousAlias(db, input, aliasId, claims, raced, `${mutationId}:review`, actor, now);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date.now()) {
|
|
7010
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7011
|
+
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
7012
|
+
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
7013
|
+
if (alias.revision !== input.expectedRevision || alias.status !== "ambiguous") throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
7014
|
+
if (input.decision === "exclude") {
|
|
7015
|
+
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
7016
|
+
await db.transact([
|
|
7017
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7018
|
+
eventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7019
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
7020
|
+
return next2;
|
|
7021
|
+
}
|
|
7022
|
+
const canonicalId = input.personId ? canonicalNetworkPersonId(input.personId, state.persons) : null;
|
|
7023
|
+
if (!canonicalId || canonicalId !== input.personId) throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
7024
|
+
const conflicting = alias.identityClaims.find((claim) => state.claims.some((row) => row.id === claim.id && row.personId !== canonicalId));
|
|
7025
|
+
if (conflicting) throw new NetworkPersonRegistryError("identity_conflict", "verified identity belongs to another Person; merge or choose that Person explicitly");
|
|
7026
|
+
const missing = alias.identityClaims.filter((claim) => !state.claims.some((row) => row.id === claim.id));
|
|
7027
|
+
const { candidatePersonIds: _candidatePersonIds, ...aliasWithoutCandidates } = alias;
|
|
7028
|
+
const next = { ...aliasWithoutCandidates, status: "linked", personId: canonicalId, revision: alias.revision + 1, updatedAt: now };
|
|
7029
|
+
await db.transact([
|
|
7030
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next } },
|
|
7031
|
+
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7032
|
+
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7033
|
+
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7034
|
+
eventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7035
|
+
], { mutationId, guards: [
|
|
7036
|
+
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7037
|
+
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
7038
|
+
] });
|
|
7039
|
+
return next;
|
|
7040
|
+
}
|
|
7041
|
+
async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now()) {
|
|
7042
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7043
|
+
const source = state.persons.find((person) => person.id === input.sourcePersonId);
|
|
7044
|
+
const target = state.persons.find((person) => person.id === input.targetPersonId);
|
|
7045
|
+
if (!source || !target || source.id === target.id || source.state !== "active" || target.state !== "active") throw new NetworkPersonRegistryError("invalid_merge", "merge requires two distinct active Persons");
|
|
7046
|
+
if (source.revision !== input.expectedRevision || target.revision !== input.expectedTargetRevision || canonicalNetworkPersonId(target.id, state.persons) !== target.id) {
|
|
7047
|
+
throw new NetworkPersonRegistryError("stale_merge", "Person changed; reload before merging");
|
|
7048
|
+
}
|
|
7049
|
+
const sourceNext = { ...source, state: "merged", mergedIntoId: target.id, revision: source.revision + 1, updatedAt: now };
|
|
7050
|
+
const targetNext = {
|
|
7051
|
+
...target,
|
|
7052
|
+
contactPolicy: {
|
|
7053
|
+
marketingSuppressed: source.contactPolicy.marketingSuppressed || target.contactPolicy.marketingSuppressed,
|
|
7054
|
+
discretionarySuppressed: source.contactPolicy.discretionarySuppressed || target.contactPolicy.discretionarySuppressed,
|
|
7055
|
+
...target.contactPolicy.reason || source.contactPolicy.reason ? { reason: target.contactPolicy.reason ?? source.contactPolicy.reason } : {},
|
|
7056
|
+
updatedAt: now
|
|
7057
|
+
},
|
|
7058
|
+
revision: target.revision + 1,
|
|
7059
|
+
updatedAt: now
|
|
7060
|
+
};
|
|
7061
|
+
await db.transact([
|
|
7062
|
+
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7063
|
+
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7064
|
+
eventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7065
|
+
], { mutationId, guards: [
|
|
7066
|
+
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7067
|
+
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
7068
|
+
] });
|
|
7069
|
+
return { source: sourceNext, target: targetNext };
|
|
7070
|
+
}
|
|
7071
|
+
async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7072
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7073
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7074
|
+
if (!person || person.state !== "merged" || person.revision !== input.expectedRevision) throw new NetworkPersonRegistryError("stale_unmerge", "merged Person changed; reload before unmerging");
|
|
7075
|
+
const next = { ...person, state: "active", revision: person.revision + 1, updatedAt: now };
|
|
7076
|
+
delete next.mergedIntoId;
|
|
7077
|
+
await db.transact([
|
|
7078
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7079
|
+
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7080
|
+
eventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7081
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7082
|
+
return next;
|
|
7083
|
+
}
|
|
7084
|
+
async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7085
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7086
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7087
|
+
if (!person || person.state !== "active" || canonicalNetworkPersonId(person.id, state.persons) !== person.id || person.revision !== input.expectedRevision) {
|
|
7088
|
+
throw new NetworkPersonRegistryError("stale_suppression", "active canonical Person changed; reload before updating suppression");
|
|
7089
|
+
}
|
|
7090
|
+
const policy = { marketingSuppressed: input.marketingSuppressed, discretionarySuppressed: input.discretionarySuppressed, ...input.reason ? { reason: input.reason } : {}, updatedAt: now };
|
|
7091
|
+
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7092
|
+
await db.transact([
|
|
7093
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7094
|
+
eventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7095
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7096
|
+
return next;
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
// src/network-person-reveal.ts
|
|
7100
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7101
|
+
const reason = input.reason.trim();
|
|
7102
|
+
if (!reason || reason.length > 500) {
|
|
7103
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7104
|
+
}
|
|
7105
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7106
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7107
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7108
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7109
|
+
const event = {
|
|
7110
|
+
t: "update",
|
|
7111
|
+
ns: "networkPersonEvents",
|
|
7112
|
+
id: `network_person_event:${mutationId}`,
|
|
7113
|
+
attrs: {
|
|
7114
|
+
id: `network_person_event:${mutationId}`,
|
|
7115
|
+
kind: "pii_revealed",
|
|
7116
|
+
actorId: actor.id,
|
|
7117
|
+
...ids,
|
|
7118
|
+
mutationId,
|
|
7119
|
+
detail: { targetKind: input.kind, reason },
|
|
7120
|
+
createdAt: now
|
|
7121
|
+
}
|
|
7122
|
+
};
|
|
7123
|
+
await db.transact([event], {
|
|
7124
|
+
mutationId,
|
|
7125
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7126
|
+
});
|
|
7127
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7128
|
+
}
|
|
7129
|
+
|
|
7130
|
+
// src/worker-routes-network-person-registry.ts
|
|
7131
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7132
|
+
var MAX_BODY2 = 32e3;
|
|
7133
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7134
|
+
function redactedName(displayName) {
|
|
7135
|
+
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7136
|
+
}
|
|
7137
|
+
var redactedLink = (link) => ({
|
|
7138
|
+
id: link.id,
|
|
7139
|
+
personId: link.personId,
|
|
7140
|
+
chapterId: link.chapterId,
|
|
7141
|
+
state: link.state,
|
|
7142
|
+
createdAt: link.createdAt
|
|
7143
|
+
});
|
|
7144
|
+
var safePolicy = (policy) => ({
|
|
7145
|
+
marketingSuppressed: policy.marketingSuppressed,
|
|
7146
|
+
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7147
|
+
hasReason: Boolean(policy.reason),
|
|
7148
|
+
updatedAt: policy.updatedAt
|
|
7149
|
+
});
|
|
7150
|
+
var safeAlias = (alias) => ({
|
|
7151
|
+
id: alias.id,
|
|
7152
|
+
sourceSystem: alias.sourceSystem,
|
|
7153
|
+
displayNameHint: redactedName(alias.displayName),
|
|
7154
|
+
status: alias.status,
|
|
7155
|
+
personId: alias.personId ?? null,
|
|
7156
|
+
candidatePersonIds: alias.candidatePersonIds ?? [],
|
|
7157
|
+
consent: {
|
|
7158
|
+
marketing: alias.consent.marketing,
|
|
7159
|
+
discretionary: alias.consent.discretionary,
|
|
7160
|
+
recordedAt: alias.consent.recordedAt,
|
|
7161
|
+
hasBasis: Boolean(alias.consent.basis)
|
|
7162
|
+
},
|
|
7163
|
+
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7164
|
+
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7165
|
+
revision: alias.revision,
|
|
7166
|
+
updatedAt: alias.updatedAt
|
|
7167
|
+
});
|
|
7168
|
+
var safePerson = (person) => ({
|
|
7169
|
+
id: person.id,
|
|
7170
|
+
displayNameHint: redactedName(person.displayName),
|
|
7171
|
+
state: person.state,
|
|
7172
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7173
|
+
revision: person.revision,
|
|
7174
|
+
contactPolicy: safePolicy(person.contactPolicy)
|
|
7175
|
+
});
|
|
7176
|
+
var positiveRevision = (value) => {
|
|
7177
|
+
const parsed = Number(value);
|
|
7178
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
7179
|
+
};
|
|
7180
|
+
async function gate10(req, env, ctx) {
|
|
7181
|
+
const db = ctx.makeDb(env);
|
|
7182
|
+
const user = await ctx.verifyUser(req, env);
|
|
7183
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7184
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7185
|
+
return { db, user };
|
|
7186
|
+
}
|
|
7187
|
+
function registryError(error) {
|
|
7188
|
+
if (error instanceof NetworkPersonRegistryError) {
|
|
7189
|
+
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7190
|
+
return json({ error: error.message, code: error.code }, status);
|
|
7191
|
+
}
|
|
7192
|
+
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
7193
|
+
return json({ error: "registry changed; reload and retry", code: "stale_write" }, 409);
|
|
7194
|
+
}
|
|
7195
|
+
return null;
|
|
7196
|
+
}
|
|
7197
|
+
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
7198
|
+
if (url.pathname !== PATH2) return null;
|
|
7199
|
+
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
7200
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7201
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
7202
|
+
return json({ error: "request is too large" }, 413);
|
|
7203
|
+
}
|
|
7204
|
+
const got = await gate10(req, env, ctx);
|
|
7205
|
+
if ("response" in got) return got.response;
|
|
7206
|
+
const db = got.db;
|
|
7207
|
+
if (req.method === "POST") {
|
|
7208
|
+
let body;
|
|
7209
|
+
try {
|
|
7210
|
+
const text5 = await req.text();
|
|
7211
|
+
if (new TextEncoder().encode(text5).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7212
|
+
const parsed = JSON.parse(text5);
|
|
7213
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7214
|
+
body = parsed;
|
|
7215
|
+
} catch {
|
|
7216
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7217
|
+
}
|
|
7218
|
+
const command = typeof body.command === "string" ? body.command : "";
|
|
7219
|
+
const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
|
|
7220
|
+
const input = body.input && typeof body.input === "object" && !Array.isArray(body.input) ? body.input : {};
|
|
7221
|
+
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7222
|
+
const actionActor = { id: got.user.userId };
|
|
7223
|
+
try {
|
|
7224
|
+
if (command === "import") {
|
|
7225
|
+
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7226
|
+
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|
|
7227
|
+
const result = await importNetworkPerson(db, validated.value, mutationId, actionActor);
|
|
7228
|
+
return json({
|
|
7229
|
+
result: {
|
|
7230
|
+
status: result.status,
|
|
7231
|
+
alias: safeAlias(result.alias),
|
|
7232
|
+
..."person" in result && result.person ? { person: safePerson(result.person) } : {},
|
|
7233
|
+
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7234
|
+
}
|
|
7235
|
+
}, 201);
|
|
7236
|
+
}
|
|
7237
|
+
if (command === "review") {
|
|
7238
|
+
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or exclude" }, 400);
|
|
7239
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7240
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7241
|
+
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
7242
|
+
const result = await reviewNetworkPersonAlias(db, {
|
|
7243
|
+
aliasId: typeof input.aliasId === "string" ? input.aliasId : "",
|
|
7244
|
+
expectedRevision,
|
|
7245
|
+
decision: input.decision,
|
|
7246
|
+
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7247
|
+
}, mutationId, actionActor);
|
|
7248
|
+
return json({ result: safeAlias(result) });
|
|
7249
|
+
}
|
|
7250
|
+
if (command === "merge") {
|
|
7251
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7252
|
+
const expectedTargetRevision = positiveRevision(input.expectedTargetRevision);
|
|
7253
|
+
if (!expectedRevision || !expectedTargetRevision) return json({ error: "source and target revisions are required" }, 400);
|
|
7254
|
+
const result = await mergeNetworkPersons(db, {
|
|
7255
|
+
sourcePersonId: typeof input.sourcePersonId === "string" ? input.sourcePersonId : "",
|
|
7256
|
+
targetPersonId: typeof input.targetPersonId === "string" ? input.targetPersonId : "",
|
|
7257
|
+
expectedRevision,
|
|
7258
|
+
expectedTargetRevision
|
|
7259
|
+
}, mutationId, actionActor);
|
|
7260
|
+
return json({ result: { source: safePerson(result.source), target: safePerson(result.target) } });
|
|
7261
|
+
}
|
|
7262
|
+
if (command === "unmerge") {
|
|
7263
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7264
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7265
|
+
const result = await unmergeNetworkPerson(db, {
|
|
7266
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7267
|
+
expectedRevision
|
|
7268
|
+
}, mutationId, actionActor);
|
|
7269
|
+
return json({ result: safePerson(result) });
|
|
7270
|
+
}
|
|
7271
|
+
if (command === "suppression") {
|
|
7272
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7273
|
+
if (!expectedRevision || typeof input.marketingSuppressed !== "boolean" || typeof input.discretionarySuppressed !== "boolean") {
|
|
7274
|
+
return json({ error: "expectedRevision and both suppression flags are required" }, 400);
|
|
7275
|
+
}
|
|
7276
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7277
|
+
if (reason.length > 500) return json({ error: "suppression reason is too long" }, 400);
|
|
7278
|
+
const result = await suppressNetworkPerson(db, {
|
|
7279
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7280
|
+
expectedRevision,
|
|
7281
|
+
marketingSuppressed: input.marketingSuppressed,
|
|
7282
|
+
discretionarySuppressed: input.discretionarySuppressed,
|
|
7283
|
+
...reason ? { reason } : {}
|
|
7284
|
+
}, mutationId, actionActor);
|
|
7285
|
+
return json({ result: safePerson(result) });
|
|
7286
|
+
}
|
|
7287
|
+
if (command === "reveal") {
|
|
7288
|
+
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
7289
|
+
const result = await revealNetworkPersonRegistryRecord(db, {
|
|
7290
|
+
kind: input.kind,
|
|
7291
|
+
id: typeof input.id === "string" ? input.id : "",
|
|
7292
|
+
reason: typeof input.reason === "string" ? input.reason : ""
|
|
7293
|
+
}, mutationId, actionActor);
|
|
7294
|
+
return json({ sensitive: true, result });
|
|
7295
|
+
}
|
|
7296
|
+
return json({ error: "unknown registry command" }, 400);
|
|
7297
|
+
} catch (error) {
|
|
7298
|
+
const response = registryError(error);
|
|
7299
|
+
if (response) return response;
|
|
7300
|
+
throw error;
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7304
|
+
const persons = state.persons.map((person) => {
|
|
7305
|
+
const links = state.links.filter((link) => link.personId === person.id);
|
|
7306
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7307
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7308
|
+
return {
|
|
7309
|
+
id: person.id,
|
|
7310
|
+
displayNameHint: redactedName(person.displayName),
|
|
7311
|
+
state: person.state,
|
|
7312
|
+
canonicalPersonId: canonicalId,
|
|
7313
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7314
|
+
revision: person.revision,
|
|
7315
|
+
contactPolicy: safePolicy(person.contactPolicy),
|
|
7316
|
+
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7317
|
+
chapterLinks: links.map(redactedLink),
|
|
7318
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, links.map(redactedLink)).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7319
|
+
};
|
|
7320
|
+
});
|
|
7321
|
+
const aliases = state.aliases.map(safeAlias);
|
|
7322
|
+
return json({
|
|
7323
|
+
version: 1,
|
|
7324
|
+
mode: "shadow",
|
|
7325
|
+
writable: true,
|
|
7326
|
+
authoritativeForChapterOperation: false,
|
|
7327
|
+
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7328
|
+
persons,
|
|
7329
|
+
aliases,
|
|
7330
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7331
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7332
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7333
|
+
id: event.id,
|
|
7334
|
+
kind: event.kind,
|
|
7335
|
+
personId: event.personId ?? null,
|
|
7336
|
+
aliasId: event.aliasId ?? null,
|
|
7337
|
+
actorId: event.actorId,
|
|
7338
|
+
mutationId: event.mutationId,
|
|
7339
|
+
createdAt: event.createdAt
|
|
7340
|
+
}))
|
|
7341
|
+
});
|
|
7342
|
+
};
|
|
7343
|
+
|
|
7344
|
+
// src/worker-routes-network-commercial.ts
|
|
7345
|
+
var import_stripe3 = require("@odla-ai/stripe");
|
|
7346
|
+
|
|
7347
|
+
// src/config-network-commercial.ts
|
|
7348
|
+
function networkCommercialSourceKey(source) {
|
|
7349
|
+
return [source.stripeMode, source.appId, source.environment, source.dataEnvironment, source.runtime, source.chapterId].join(":");
|
|
7350
|
+
}
|
|
7351
|
+
|
|
7352
|
+
// src/network-commercial.ts
|
|
7353
|
+
var raw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7354
|
+
var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 ? value : void 0;
|
|
7355
|
+
var integer2 = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
7356
|
+
var bool = (value) => typeof value === "boolean" ? value : void 0;
|
|
7357
|
+
var providerId = (value, prefix) => {
|
|
7358
|
+
const valueText = text3(value);
|
|
7359
|
+
return valueText && (!prefix || valueText.startsWith(prefix)) ? valueText : void 0;
|
|
7360
|
+
};
|
|
7361
|
+
function metadataCandidates(object) {
|
|
7362
|
+
const parent = raw(object.parent);
|
|
7363
|
+
const subscriptionDetails = raw(object.subscription_details);
|
|
7364
|
+
const parentSubscription = raw(parent.subscription_details);
|
|
7365
|
+
return [raw(object.metadata), raw(subscriptionDetails.metadata), raw(parentSubscription.metadata)];
|
|
7366
|
+
}
|
|
7367
|
+
function metadataValue(candidates, key) {
|
|
7368
|
+
return candidates.map((candidate) => text3(candidate[key])).find(Boolean);
|
|
7369
|
+
}
|
|
7370
|
+
function refId(value, prefix) {
|
|
7371
|
+
if (typeof value === "string") return providerId(value, prefix);
|
|
7372
|
+
return providerId(raw(value).id, prefix);
|
|
7373
|
+
}
|
|
7374
|
+
function subscriptionRef(object) {
|
|
7375
|
+
return refId(object.subscription, "sub_") ?? refId(raw(object.subscription_details).subscription, "sub_") ?? refId(raw(raw(object.parent).subscription_details).subscription, "sub_");
|
|
7376
|
+
}
|
|
7377
|
+
function firstPrice(object) {
|
|
7378
|
+
const data = raw(object.items).data;
|
|
7379
|
+
const item = Array.isArray(data) ? raw(data[0]) : {};
|
|
7380
|
+
const priceId = refId(item.price, "price_");
|
|
7381
|
+
const quantity = integer2(item.quantity);
|
|
7382
|
+
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
7383
|
+
}
|
|
7384
|
+
function normalizeNetworkCommercialStripeEvent(input, digest2) {
|
|
7385
|
+
const event = raw(input);
|
|
7386
|
+
const eventId = providerId(event.id, "evt_") ?? "";
|
|
7387
|
+
const eventType = text3(event.type) ?? "";
|
|
7388
|
+
const object = raw(raw(event.data).object);
|
|
7389
|
+
const objectId = providerId(object.id) ?? "";
|
|
7390
|
+
const candidates = metadataCandidates(object);
|
|
7391
|
+
const identity = {};
|
|
7392
|
+
const identityKeys = {
|
|
7393
|
+
appId: "odlaAppId",
|
|
7394
|
+
chapterId: "chapterId",
|
|
7395
|
+
environment: "odlaEnvironment",
|
|
7396
|
+
dataEnvironment: "odlaDataEnvironment",
|
|
7397
|
+
runtime: "odlaRuntime"
|
|
7398
|
+
};
|
|
7399
|
+
for (const [target, source] of Object.entries(identityKeys)) {
|
|
7400
|
+
const value = metadataValue(candidates, source);
|
|
7401
|
+
if (value) identity[target] = value;
|
|
7402
|
+
}
|
|
7403
|
+
let objectKind = "ignored";
|
|
7404
|
+
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
7405
|
+
else if (eventType.startsWith("customer.")) objectKind = "customer";
|
|
7406
|
+
else if (eventType.startsWith("invoice.")) objectKind = "invoice";
|
|
7407
|
+
else if (eventType.startsWith("refund.")) objectKind = "refund";
|
|
7408
|
+
else if (eventType === "charge.refunded") objectKind = "refund";
|
|
7409
|
+
const customerId = refId(object.customer, "cus_");
|
|
7410
|
+
const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object);
|
|
7411
|
+
const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object.invoice, "in_");
|
|
7412
|
+
const chargeId = eventType === "charge.refunded" ? providerId(objectId, "ch_") : refId(object.charge, "ch_");
|
|
7413
|
+
const paymentIntentId = refId(object.payment_intent, "pi_");
|
|
7414
|
+
const price = firstPrice(object);
|
|
7415
|
+
const amountCents = integer2(eventType === "charge.refunded" ? object.amount_refunded : object.amount);
|
|
7416
|
+
const currency = text3(object.currency)?.toLowerCase();
|
|
7417
|
+
const status = text3(object.status);
|
|
7418
|
+
return {
|
|
7419
|
+
eventId,
|
|
7420
|
+
digest: digest2,
|
|
7421
|
+
eventType,
|
|
7422
|
+
eventCreated: integer2(event.created) ?? 0,
|
|
7423
|
+
livemode: event.livemode === true,
|
|
7424
|
+
objectKind,
|
|
7425
|
+
objectId,
|
|
7426
|
+
identity,
|
|
7427
|
+
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
7428
|
+
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
7429
|
+
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
7430
|
+
...customerId ? { customerId } : {},
|
|
7431
|
+
...subscriptionId ? { subscriptionId } : {},
|
|
7432
|
+
...invoiceId ? { invoiceId } : {},
|
|
7433
|
+
...chargeId ? { chargeId } : {},
|
|
7434
|
+
...paymentIntentId ? { paymentIntentId } : {},
|
|
7435
|
+
...price,
|
|
7436
|
+
...status ? { status } : {},
|
|
7437
|
+
...currency ? { currency } : {},
|
|
7438
|
+
...amountCents !== void 0 ? { amountCents } : {},
|
|
7439
|
+
...integer2(object.amount_paid) !== void 0 ? { amountPaidCents: integer2(object.amount_paid) } : {},
|
|
7440
|
+
...integer2(object.amount_remaining) !== void 0 ? { amountRemainingCents: integer2(object.amount_remaining) } : {},
|
|
7441
|
+
...integer2(object.current_period_start) !== void 0 ? { currentPeriodStart: integer2(object.current_period_start) } : {},
|
|
7442
|
+
...integer2(object.current_period_end) !== void 0 ? { currentPeriodEnd: integer2(object.current_period_end) } : {},
|
|
7443
|
+
...bool(object.cancel_at_period_end) !== void 0 ? { cancelAtPeriodEnd: bool(object.cancel_at_period_end) } : {}
|
|
7444
|
+
};
|
|
7445
|
+
}
|
|
7446
|
+
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
7447
|
+
function directSource(fact, sources) {
|
|
7448
|
+
const mode = fact.livemode ? "live" : "test";
|
|
7449
|
+
const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
|
|
7450
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
|
|
7451
|
+
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
7452
|
+
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
7453
|
+
return null;
|
|
7454
|
+
}
|
|
7455
|
+
var objectRefs = (fact) => [
|
|
7456
|
+
fact.customerId && `customer:${fact.customerId}`,
|
|
7457
|
+
fact.subscriptionId && `subscription:${fact.subscriptionId}`,
|
|
7458
|
+
fact.invoiceId && `invoice:${fact.invoiceId}`,
|
|
7459
|
+
fact.chargeId && `charge:${fact.chargeId}`,
|
|
7460
|
+
fact.paymentIntentId && `payment_intent:${fact.paymentIntentId}`
|
|
7461
|
+
].filter((value) => Boolean(value));
|
|
7462
|
+
function ownRef(fact) {
|
|
7463
|
+
if (!fact.objectId) return null;
|
|
7464
|
+
if (fact.objectKind === "customer") return `customer:${fact.objectId}`;
|
|
7465
|
+
if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
|
|
7466
|
+
if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
|
|
7467
|
+
if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
|
|
7468
|
+
return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
|
|
7469
|
+
}
|
|
7470
|
+
var order = (left, right) => left.eventCreated - right.eventCreated || left.eventId.localeCompare(right.eventId);
|
|
7471
|
+
function projectNetworkCommercialEvents(input, sources) {
|
|
7472
|
+
const facts2 = [...input].sort(order);
|
|
7473
|
+
const exceptions = [];
|
|
7474
|
+
const unique = [];
|
|
7475
|
+
const digests = /* @__PURE__ */ new Map();
|
|
7476
|
+
for (const fact of facts2) {
|
|
7477
|
+
const prior = digests.get(fact.eventId);
|
|
7478
|
+
if (prior === void 0) {
|
|
7479
|
+
digests.set(fact.eventId, fact.digest);
|
|
7480
|
+
unique.push(fact);
|
|
7481
|
+
} else if (prior !== fact.digest) exceptions.push({ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7482
|
+
}
|
|
7483
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
7484
|
+
const refs = /* @__PURE__ */ new Map();
|
|
7485
|
+
for (const fact of unique) {
|
|
7486
|
+
const direct = directSource(fact, sources);
|
|
7487
|
+
if (direct && direct !== "wrong") assignments.set(fact.eventId, direct);
|
|
7488
|
+
}
|
|
7489
|
+
let changed = true;
|
|
7490
|
+
while (changed) {
|
|
7491
|
+
changed = false;
|
|
7492
|
+
for (const fact of unique) {
|
|
7493
|
+
const assigned = assignments.get(fact.eventId);
|
|
7494
|
+
if (assigned) {
|
|
7495
|
+
const key = networkCommercialSourceKey(assigned);
|
|
7496
|
+
for (const ref of [...objectRefs(fact), ownRef(fact)].filter((value) => Boolean(value))) {
|
|
7497
|
+
const keys2 = refs.get(ref) ?? /* @__PURE__ */ new Set();
|
|
7498
|
+
const before = keys2.size;
|
|
7499
|
+
keys2.add(key);
|
|
7500
|
+
refs.set(ref, keys2);
|
|
7501
|
+
if (keys2.size !== before) changed = true;
|
|
7502
|
+
}
|
|
7503
|
+
continue;
|
|
7504
|
+
}
|
|
7505
|
+
if (directSource(fact, sources) === "wrong") continue;
|
|
7506
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
7507
|
+
if (keys.size !== 1) continue;
|
|
7508
|
+
const candidate = sources.find((source) => networkCommercialSourceKey(source) === [...keys][0]);
|
|
7509
|
+
if (candidate && directSource({ ...fact, identity: fact.identity }, [candidate]) !== "wrong") {
|
|
7510
|
+
assignments.set(fact.eventId, candidate);
|
|
7511
|
+
changed = true;
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
}
|
|
7515
|
+
const latest = /* @__PURE__ */ new Map();
|
|
7516
|
+
for (const fact of unique) {
|
|
7517
|
+
if (fact.objectKind === "ignored" || !fact.eventId || !fact.objectId) {
|
|
7518
|
+
exceptions.push({ eventId: fact.eventId, kind: "unsupported", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
7521
|
+
const source = assignments.get(fact.eventId);
|
|
7522
|
+
if (!source) {
|
|
7523
|
+
const direct = directSource(fact, sources);
|
|
7524
|
+
const keys = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
|
|
7525
|
+
exceptions.push({ eventId: fact.eventId, kind: direct === "wrong" ? "wrong_partition" : keys.size > 1 ? "ambiguous_partition" : "unmatched_partition", detail: { eventType: fact.eventType, objectId: fact.objectId } });
|
|
7526
|
+
continue;
|
|
7527
|
+
}
|
|
7528
|
+
const row = { ...fact, sourceKey: networkCommercialSourceKey(source) };
|
|
7529
|
+
const key = `${fact.objectKind}:${fact.objectId}`;
|
|
7530
|
+
const prior = latest.get(key);
|
|
7531
|
+
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7532
|
+
}
|
|
7533
|
+
const rows6 = [...latest.values()];
|
|
7534
|
+
const customers = rows6.filter((row) => row.objectKind === "customer");
|
|
7535
|
+
const subscriptions = rows6.filter((row) => row.objectKind === "subscription");
|
|
7536
|
+
const invoices = rows6.filter((row) => row.objectKind === "invoice");
|
|
7537
|
+
const refunds = rows6.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
7538
|
+
for (const charge of rows6.filter((row) => row.eventType === "charge.refunded")) {
|
|
7539
|
+
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7540
|
+
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
const totalsMap = /* @__PURE__ */ new Map();
|
|
7544
|
+
const add = (row, field, amount) => {
|
|
7545
|
+
if (!row.currency) return;
|
|
7546
|
+
const key = `${row.sourceKey}:${row.currency}`;
|
|
7547
|
+
const total = totalsMap.get(key) ?? { sourceKey: row.sourceKey, currency: row.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
7548
|
+
total[field] += amount;
|
|
7549
|
+
total.netCents = total.paidCents - total.refundedCents;
|
|
7550
|
+
totalsMap.set(key, total);
|
|
7551
|
+
};
|
|
7552
|
+
for (const row of invoices) add(row, "paidCents", row.amountPaidCents ?? 0);
|
|
7553
|
+
for (const row of refunds) add(row, "refundedCents", row.amountCents ?? 0);
|
|
7554
|
+
return { customers, subscriptions, invoices, refunds, exceptions, totals: [...totalsMap.values()].sort((a, b) => `${a.sourceKey}:${a.currency}`.localeCompare(`${b.sourceKey}:${b.currency}`)) };
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
// src/network-commercial-store.ts
|
|
7558
|
+
var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
|
|
7559
|
+
var projectionId = (kind, sourceKey, providerId2) => `network-commercial:${kind}:${sourceKey}:${providerId2}`;
|
|
7560
|
+
var exceptionId = (eventId, kind) => `network-commercial-exception:${eventId}:${kind}`;
|
|
7561
|
+
var materializedNamespaces = [
|
|
7562
|
+
"networkCommercialCustomers",
|
|
7563
|
+
"networkCommercialSubscriptions",
|
|
7564
|
+
"networkCommercialInvoices",
|
|
7565
|
+
"networkCommercialRefunds"
|
|
7566
|
+
];
|
|
7567
|
+
async function rows5(db, ns) {
|
|
7568
|
+
const all = [];
|
|
7569
|
+
const pageSize = 500;
|
|
7570
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
7571
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset } } });
|
|
7572
|
+
const page = Array.isArray(result[ns]) ? result[ns] : [];
|
|
7573
|
+
all.push(...page);
|
|
7574
|
+
if (page.length < pageSize) return all;
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7577
|
+
async function loadNetworkCommercialProjection(db, sources) {
|
|
7578
|
+
const stored = await rows5(db, "networkCommercialEvents");
|
|
7579
|
+
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7580
|
+
const persisted = await rows5(db, "networkCommercialExceptions");
|
|
7581
|
+
for (const row of persisted) {
|
|
7582
|
+
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7583
|
+
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
7584
|
+
projection.exceptions.push({
|
|
7585
|
+
eventId: String(row.eventId),
|
|
7586
|
+
kind: "collision",
|
|
7587
|
+
...typeof row.sourceKey === "string" ? { sourceKey: row.sourceKey } : {},
|
|
7588
|
+
detail: row.detail
|
|
7589
|
+
});
|
|
7590
|
+
}
|
|
7591
|
+
return projection;
|
|
7592
|
+
}
|
|
7593
|
+
function projectionOps(projection, now) {
|
|
7594
|
+
const groups = [
|
|
7595
|
+
["networkCommercialCustomers", "customer", projection.customers],
|
|
7596
|
+
["networkCommercialSubscriptions", "subscription", projection.subscriptions],
|
|
7597
|
+
["networkCommercialInvoices", "invoice", projection.invoices],
|
|
7598
|
+
["networkCommercialRefunds", "refund", projection.refunds]
|
|
7599
|
+
];
|
|
7600
|
+
return groups.flatMap(([ns, kind, projected]) => projected.map((snapshot) => ({
|
|
7601
|
+
t: "update",
|
|
7602
|
+
ns,
|
|
7603
|
+
id: projectionId(kind, snapshot.sourceKey, snapshot.objectId),
|
|
7604
|
+
attrs: {
|
|
7605
|
+
id: projectionId(kind, snapshot.sourceKey, snapshot.objectId),
|
|
7606
|
+
providerId: snapshot.objectId,
|
|
7607
|
+
sourceKey: snapshot.sourceKey,
|
|
7608
|
+
status: "active",
|
|
7609
|
+
snapshot,
|
|
7610
|
+
eventCreated: snapshot.eventCreated,
|
|
7611
|
+
updatedAt: now
|
|
7612
|
+
}
|
|
7613
|
+
})));
|
|
7614
|
+
}
|
|
7615
|
+
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7616
|
+
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7617
|
+
const [existing, ...materialized] = await Promise.all([
|
|
7618
|
+
rows5(db, "networkCommercialExceptions"),
|
|
7619
|
+
...materializedNamespaces.map((ns) => rows5(db, ns))
|
|
7620
|
+
]);
|
|
7621
|
+
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7622
|
+
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
7623
|
+
const ops = [
|
|
7624
|
+
...projectionOps(projection, now),
|
|
7625
|
+
...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 } }))),
|
|
7626
|
+
...existing.filter((row) => row.status === "active" && !activeIds.has(String(row.id))).map((row) => ({
|
|
7627
|
+
t: "update",
|
|
7628
|
+
ns: "networkCommercialExceptions",
|
|
7629
|
+
id: String(row.id),
|
|
7630
|
+
attrs: { status: "resolved", updatedAt: now }
|
|
7631
|
+
})),
|
|
7632
|
+
...projection.exceptions.map((item) => ({
|
|
7633
|
+
t: "update",
|
|
7634
|
+
ns: "networkCommercialExceptions",
|
|
7635
|
+
id: exceptionId(item.eventId, item.kind),
|
|
7636
|
+
attrs: {
|
|
7637
|
+
id: exceptionId(item.eventId, item.kind),
|
|
7638
|
+
eventId: item.eventId,
|
|
7639
|
+
kind: item.kind,
|
|
7640
|
+
...item.sourceKey ? { sourceKey: item.sourceKey } : {},
|
|
7641
|
+
status: "active",
|
|
7642
|
+
detail: item.detail,
|
|
7643
|
+
updatedAt: now
|
|
7644
|
+
}
|
|
7645
|
+
}))
|
|
7646
|
+
];
|
|
7647
|
+
if (ops.length > 0) await db.transact(ops, { mutationId });
|
|
7648
|
+
return projection;
|
|
7649
|
+
}
|
|
7650
|
+
async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now()) {
|
|
7651
|
+
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
7652
|
+
const existing = found.networkCommercialEvents?.[0];
|
|
7653
|
+
if (existing) {
|
|
7654
|
+
const projection2 = await loadNetworkCommercialProjection(db, sources);
|
|
7655
|
+
if (existing.digest === fact.digest) return { status: "replayed", projection: projection2 };
|
|
7656
|
+
const id2 = exceptionId(fact.eventId, "collision");
|
|
7657
|
+
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id2, attrs: {
|
|
7658
|
+
id: id2,
|
|
7659
|
+
eventId: fact.eventId,
|
|
7660
|
+
kind: "collision",
|
|
7661
|
+
status: "active",
|
|
7662
|
+
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
7663
|
+
updatedAt: now
|
|
7664
|
+
} }], { mutationId: `network-commercial-collision:${fact.eventId}:${fact.digest}` });
|
|
7665
|
+
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
7666
|
+
}
|
|
7667
|
+
const id = eventRowId(fact.eventId);
|
|
7668
|
+
let lostRace = false;
|
|
7669
|
+
let duplicate = false;
|
|
7670
|
+
try {
|
|
7671
|
+
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id, attrs: {
|
|
7672
|
+
id,
|
|
7673
|
+
eventId: fact.eventId,
|
|
7674
|
+
digest: fact.digest,
|
|
7675
|
+
eventType: fact.eventType,
|
|
7676
|
+
eventCreated: fact.eventCreated,
|
|
7677
|
+
livemode: fact.livemode,
|
|
7678
|
+
fact,
|
|
7679
|
+
receivedAt: now
|
|
7680
|
+
} }], {
|
|
7681
|
+
mutationId: `network-commercial-ingest:${fact.eventId}:${fact.digest}`,
|
|
7682
|
+
guards: [{ ns: "networkCommercialEvents", id, exists: false }]
|
|
7683
|
+
});
|
|
7684
|
+
duplicate = write.duplicate;
|
|
7685
|
+
} catch (error) {
|
|
7686
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
7687
|
+
lostRace = true;
|
|
7688
|
+
}
|
|
7689
|
+
if (duplicate || lostRace) {
|
|
7690
|
+
const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
7691
|
+
const row = raced.networkCommercialEvents?.[0];
|
|
7692
|
+
const projection2 = await loadNetworkCommercialProjection(db, sources);
|
|
7693
|
+
if (row?.digest === fact.digest) return { status: "replayed", projection: projection2 };
|
|
7694
|
+
const id2 = exceptionId(fact.eventId, "collision");
|
|
7695
|
+
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id2, attrs: {
|
|
7696
|
+
id: id2,
|
|
7697
|
+
eventId: fact.eventId,
|
|
7698
|
+
kind: "collision",
|
|
7699
|
+
status: "active",
|
|
7700
|
+
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
7701
|
+
updatedAt: now
|
|
7702
|
+
} }], { mutationId: `network-commercial-collision:${fact.eventId}:${fact.digest}` });
|
|
7703
|
+
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
7704
|
+
}
|
|
7705
|
+
const projection = await rebuildNetworkCommercialProjection(
|
|
7706
|
+
db,
|
|
7707
|
+
sources,
|
|
7708
|
+
`network-commercial-project:${fact.eventId}:${fact.digest}`,
|
|
7709
|
+
now
|
|
7710
|
+
);
|
|
7711
|
+
return { status: "stored", projection };
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
// src/network-commercial-reconcile.ts
|
|
7715
|
+
var asRaw = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
7716
|
+
async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
7717
|
+
const objects = [];
|
|
7718
|
+
let startingAfter;
|
|
7719
|
+
for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {
|
|
7720
|
+
const page = await client.request("GET", path, {
|
|
7721
|
+
...params,
|
|
7722
|
+
limit: 100,
|
|
7723
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
7724
|
+
});
|
|
7725
|
+
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7726
|
+
throw new Error("Stripe returned an invalid commercial list response");
|
|
7727
|
+
}
|
|
7728
|
+
const rows6 = page.data.map(asRaw).filter((row) => row !== null);
|
|
7729
|
+
if (rows6.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
7730
|
+
objects.push(...rows6);
|
|
7731
|
+
if (!page.has_more) return { objects, complete: true };
|
|
7732
|
+
const cursor = rows6.at(-1)?.id;
|
|
7733
|
+
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7734
|
+
startingAfter = cursor;
|
|
7735
|
+
}
|
|
7736
|
+
return { objects, complete: false };
|
|
7737
|
+
}
|
|
7738
|
+
function facts(snapshots) {
|
|
7739
|
+
const groups = [
|
|
7740
|
+
["customer.updated", snapshots.customers.objects],
|
|
7741
|
+
["customer.subscription.updated", snapshots.subscriptions.objects],
|
|
7742
|
+
["invoice.updated", snapshots.invoices.objects],
|
|
7743
|
+
["refund.updated", snapshots.refunds.objects]
|
|
7744
|
+
];
|
|
7745
|
+
return groups.flatMap(([eventType, objects]) => objects.map((object) => {
|
|
7746
|
+
const id = typeof object.id === "string" ? object.id : "invalid";
|
|
7747
|
+
return normalizeNetworkCommercialStripeEvent({
|
|
7748
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id}`,
|
|
7749
|
+
type: eventType,
|
|
7750
|
+
created: typeof object.created === "number" ? object.created : 0,
|
|
7751
|
+
livemode: object.livemode === true,
|
|
7752
|
+
data: { object }
|
|
7753
|
+
}, `stripe-snapshot:${eventType}:${id}`);
|
|
7754
|
+
}));
|
|
7755
|
+
}
|
|
7756
|
+
var selected = (row) => ({
|
|
7757
|
+
status: row.status ?? null,
|
|
7758
|
+
currency: row.currency ?? null,
|
|
7759
|
+
amountCents: row.amountCents ?? null,
|
|
7760
|
+
amountPaidCents: row.amountPaidCents ?? null,
|
|
7761
|
+
amountRemainingCents: row.amountRemainingCents ?? null,
|
|
7762
|
+
customerId: row.customerId ?? null,
|
|
7763
|
+
subscriptionId: row.subscriptionId ?? null,
|
|
7764
|
+
invoiceId: row.invoiceId ?? null,
|
|
7765
|
+
priceId: row.priceId ?? null,
|
|
7766
|
+
quantity: row.quantity ?? null,
|
|
7767
|
+
applicationId: row.applicationId ?? null,
|
|
7768
|
+
personId: row.personId ?? null,
|
|
7769
|
+
membershipId: row.membershipId ?? null,
|
|
7770
|
+
currentPeriodStart: row.currentPeriodStart ?? null,
|
|
7771
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
7772
|
+
cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? null
|
|
7773
|
+
});
|
|
7774
|
+
var index = (projection) => new Map(
|
|
7775
|
+
[...projection.customers, ...projection.subscriptions, ...projection.invoices, ...projection.refunds].map((row) => [`${row.sourceKey}:${row.objectKind}:${row.objectId}`, row])
|
|
7776
|
+
);
|
|
7777
|
+
function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
7778
|
+
const stripe = projectNetworkCommercialEvents(facts(snapshots), sources);
|
|
7779
|
+
const shadowRows = index(shadow);
|
|
7780
|
+
const stripeRows = index(stripe);
|
|
7781
|
+
const exceptions = [];
|
|
7782
|
+
const complete = Object.values(snapshots).every((snapshot) => snapshot.complete);
|
|
7783
|
+
if (!complete) exceptions.push({ kind: "stripe_snapshot_truncated", detail: {} });
|
|
7784
|
+
for (const [key, row] of shadowRows) {
|
|
7785
|
+
const other = stripeRows.get(key);
|
|
7786
|
+
if (!other) {
|
|
7787
|
+
exceptions.push({ kind: "missing_from_stripe", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
7788
|
+
} else if (JSON.stringify(selected(row)) !== JSON.stringify(selected(other))) {
|
|
7789
|
+
exceptions.push({ kind: "object_mismatch", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: { shadow: selected(row), stripe: selected(other) } });
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
for (const [key, row] of stripeRows) {
|
|
7793
|
+
if (!shadowRows.has(key)) exceptions.push({ kind: "missing_from_shadow", sourceKey: row.sourceKey, objectKind: row.objectKind, providerId: row.objectId, detail: {} });
|
|
7794
|
+
}
|
|
7795
|
+
if (JSON.stringify(shadow.totals) !== JSON.stringify(stripe.totals)) {
|
|
7796
|
+
exceptions.push({ kind: "totals_mismatch", detail: { shadow: shadow.totals, stripe: stripe.totals } });
|
|
7797
|
+
}
|
|
7798
|
+
return {
|
|
7799
|
+
ok: exceptions.length === 0,
|
|
7800
|
+
complete,
|
|
7801
|
+
ignoredObjects: stripe.exceptions.filter((item) => item.kind === "wrong_partition" || item.kind === "unmatched_partition").length,
|
|
7802
|
+
exceptions,
|
|
7803
|
+
stripeTotals: stripe.totals
|
|
7804
|
+
};
|
|
7805
|
+
}
|
|
7806
|
+
|
|
7807
|
+
// src/worker-routes-network-commercial.ts
|
|
7808
|
+
var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
|
|
7809
|
+
var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
|
|
7810
|
+
var MAX_BODY3 = 512e3;
|
|
7811
|
+
var MUTATION_ID5 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7812
|
+
async function digest(body) {
|
|
7813
|
+
const value = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
7814
|
+
return [...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7815
|
+
}
|
|
7816
|
+
async function boundedBody(req) {
|
|
7817
|
+
if (Number(req.headers.get("content-length") ?? 0) > MAX_BODY3) return null;
|
|
7818
|
+
const body = await req.text();
|
|
7819
|
+
return new TextEncoder().encode(body).byteLength <= MAX_BODY3 ? body : null;
|
|
7820
|
+
}
|
|
7821
|
+
async function gate11(req, env, ctx) {
|
|
7822
|
+
const db = ctx.makeDb(env);
|
|
7823
|
+
const user = await ctx.verifyUser(req, env);
|
|
7824
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7825
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7826
|
+
return { db, user };
|
|
7827
|
+
}
|
|
7828
|
+
function safeProjection(projection) {
|
|
7829
|
+
const row = (item) => ({
|
|
7830
|
+
sourceKey: item.sourceKey,
|
|
7831
|
+
providerId: item.objectId,
|
|
7832
|
+
eventType: item.eventType,
|
|
7833
|
+
eventCreated: item.eventCreated,
|
|
7834
|
+
status: item.status ?? null,
|
|
7835
|
+
currency: item.currency ?? null,
|
|
7836
|
+
amountCents: item.amountCents ?? null,
|
|
7837
|
+
amountPaidCents: item.amountPaidCents ?? null,
|
|
7838
|
+
amountRemainingCents: item.amountRemainingCents ?? null,
|
|
7839
|
+
customerId: item.customerId ?? null,
|
|
7840
|
+
subscriptionId: item.subscriptionId ?? null,
|
|
7841
|
+
invoiceId: item.invoiceId ?? null,
|
|
7842
|
+
priceId: item.priceId ?? null,
|
|
7843
|
+
quantity: item.quantity ?? null,
|
|
7844
|
+
applicationId: item.applicationId ?? null,
|
|
7845
|
+
personId: item.personId ?? null,
|
|
7846
|
+
membershipId: item.membershipId ?? null,
|
|
7847
|
+
currentPeriodStart: item.currentPeriodStart ?? null,
|
|
7848
|
+
currentPeriodEnd: item.currentPeriodEnd ?? null,
|
|
7849
|
+
cancelAtPeriodEnd: item.cancelAtPeriodEnd ?? null
|
|
7850
|
+
});
|
|
7851
|
+
return {
|
|
7852
|
+
version: 1,
|
|
7853
|
+
mode: "shadow",
|
|
7854
|
+
writable: false,
|
|
7855
|
+
authoritativeForCharges: false,
|
|
7856
|
+
totals: projection.totals,
|
|
7857
|
+
customers: projection.customers.map(row),
|
|
7858
|
+
subscriptions: projection.subscriptions.map(row),
|
|
7859
|
+
invoices: projection.invoices.map(row),
|
|
7860
|
+
refunds: projection.refunds.map(row),
|
|
7861
|
+
exceptions: projection.exceptions
|
|
7862
|
+
};
|
|
7863
|
+
}
|
|
7864
|
+
var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
7865
|
+
if (url.pathname !== WEBHOOK_PATH) return null;
|
|
7866
|
+
if (!ctx.chapter.networkCommercialReadModel) return json({ error: "not found" }, 404);
|
|
7867
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7868
|
+
const body = await boundedBody(req);
|
|
7869
|
+
if (body === null) return json({ error: "request is too large" }, 413);
|
|
7870
|
+
const signature = req.headers.get("stripe-signature") ?? "";
|
|
7871
|
+
const db = ctx.makeDb(env);
|
|
7872
|
+
let secret;
|
|
7873
|
+
try {
|
|
7874
|
+
secret = await db.secrets.get("network_commercial_stripe_webhook_secret");
|
|
7875
|
+
} catch {
|
|
7876
|
+
return json({ error: "commercial webhook is not configured" }, 503);
|
|
7877
|
+
}
|
|
7878
|
+
try {
|
|
7879
|
+
const event = await (0, import_stripe3.constructStripeEvent)(body, signature, secret);
|
|
7880
|
+
const fact = normalizeNetworkCommercialStripeEvent(event, await digest(body));
|
|
7881
|
+
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
7882
|
+
const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact);
|
|
7883
|
+
if (result.status === "collision") {
|
|
7884
|
+
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
7885
|
+
}
|
|
7886
|
+
return json({ ok: true, status: result.status, eventId: fact.eventId });
|
|
7887
|
+
} catch (error) {
|
|
7888
|
+
if (error instanceof import_stripe3.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
7889
|
+
throw error;
|
|
7890
|
+
}
|
|
7891
|
+
};
|
|
7892
|
+
var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
7893
|
+
if (url.pathname !== ADMIN_PATH2) return null;
|
|
7894
|
+
if (!ctx.chapter.networkCommercialReadModel) return json({ error: "not found" }, 404);
|
|
7895
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7896
|
+
const got = await gate11(req, env, ctx);
|
|
7897
|
+
if ("response" in got) return got.response;
|
|
7898
|
+
const db = got.db;
|
|
7899
|
+
if (req.method === "POST") {
|
|
7900
|
+
const body = await boundedBody(req);
|
|
7901
|
+
if (body === null) return json({ error: "request is too large" }, 413);
|
|
7902
|
+
let parsed;
|
|
7903
|
+
try {
|
|
7904
|
+
const value = JSON.parse(body);
|
|
7905
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("shape");
|
|
7906
|
+
parsed = value;
|
|
7907
|
+
} catch {
|
|
7908
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7909
|
+
}
|
|
7910
|
+
if (parsed.command === "reconcile-stripe") {
|
|
7911
|
+
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
7912
|
+
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
7913
|
+
try {
|
|
7914
|
+
const stripe = (0, import_stripe3.initStripe)({ secretKey });
|
|
7915
|
+
const [customers, subscriptions, invoices, refunds] = await Promise.all([
|
|
7916
|
+
listStripeObjects(stripe, "/v1/customers"),
|
|
7917
|
+
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
7918
|
+
listStripeObjects(stripe, "/v1/invoices"),
|
|
7919
|
+
listStripeObjects(stripe, "/v1/refunds")
|
|
7920
|
+
]);
|
|
7921
|
+
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
7922
|
+
return json({
|
|
7923
|
+
reconciled: true,
|
|
7924
|
+
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds }, ctx.chapter.networkCommercialReadModel.sources)
|
|
7925
|
+
});
|
|
7926
|
+
} catch (error) {
|
|
7927
|
+
if (error instanceof import_stripe3.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
7928
|
+
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
7932
|
+
return json({ error: "rebuild or reconcile-stripe command is required; rebuild also requires a valid mutationId" }, 400);
|
|
7933
|
+
}
|
|
7934
|
+
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
7935
|
+
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
7936
|
+
}
|
|
7937
|
+
return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
|
|
7938
|
+
};
|
|
7939
|
+
|
|
6754
7940
|
// src/worker-routes-formation.ts
|
|
6755
7941
|
var import_crm11 = require("@odla-ai/crm");
|
|
6756
7942
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
@@ -6767,11 +7953,11 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
6767
7953
|
});
|
|
6768
7954
|
}
|
|
6769
7955
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|
|
6770
|
-
const
|
|
6771
|
-
if (
|
|
7956
|
+
const raw2 = await req.text();
|
|
7957
|
+
if (raw2.length > formation.bodyCap) return json({ error: "request body too large" }, 413);
|
|
6772
7958
|
let body;
|
|
6773
7959
|
try {
|
|
6774
|
-
body = JSON.parse(
|
|
7960
|
+
body = JSON.parse(raw2);
|
|
6775
7961
|
} catch {
|
|
6776
7962
|
return json({ error: "invalid JSON body" }, 400);
|
|
6777
7963
|
}
|
|
@@ -6855,7 +8041,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
6855
8041
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
6856
8042
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
6857
8043
|
}
|
|
6858
|
-
function
|
|
8044
|
+
function text4(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
6859
8045
|
if (value === void 0 && !required) return void 0;
|
|
6860
8046
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
6861
8047
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -6865,23 +8051,23 @@ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
|
6865
8051
|
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
6866
8052
|
return result;
|
|
6867
8053
|
}
|
|
6868
|
-
function
|
|
8054
|
+
function integer3(value, path, errors, minimum = 1) {
|
|
6869
8055
|
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
6870
8056
|
return Number(value);
|
|
6871
8057
|
}
|
|
6872
8058
|
function scopeValue(value, path, errors) {
|
|
6873
|
-
const
|
|
6874
|
-
if (!
|
|
8059
|
+
const raw2 = record3(value);
|
|
8060
|
+
if (!raw2) {
|
|
6875
8061
|
errors.push(`${path} must be an object`);
|
|
6876
8062
|
return null;
|
|
6877
8063
|
}
|
|
6878
|
-
unknownKeys3(
|
|
6879
|
-
if (
|
|
6880
|
-
if (
|
|
6881
|
-
return { kind:
|
|
8064
|
+
unknownKeys3(raw2, SCOPE_KEYS, path, errors);
|
|
8065
|
+
if (raw2.kind === "global" || raw2.kind === "built_not_found") {
|
|
8066
|
+
if (raw2.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
8067
|
+
return { kind: raw2.kind };
|
|
6882
8068
|
}
|
|
6883
|
-
if (
|
|
6884
|
-
const chapterId =
|
|
8069
|
+
if (raw2.kind === "chapter") {
|
|
8070
|
+
const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
6885
8071
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
6886
8072
|
return { kind: "chapter", chapterId };
|
|
6887
8073
|
}
|
|
@@ -6889,28 +8075,28 @@ function scopeValue(value, path, errors) {
|
|
|
6889
8075
|
return null;
|
|
6890
8076
|
}
|
|
6891
8077
|
function resourceValue(value, path, errors) {
|
|
6892
|
-
const
|
|
6893
|
-
if (!
|
|
8078
|
+
const raw2 = record3(value);
|
|
8079
|
+
if (!raw2) {
|
|
6894
8080
|
errors.push(`${path} must be an object`);
|
|
6895
8081
|
return null;
|
|
6896
8082
|
}
|
|
6897
|
-
unknownKeys3(
|
|
6898
|
-
const id =
|
|
8083
|
+
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
8084
|
+
const id = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
6899
8085
|
if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
6900
|
-
const kind =
|
|
8086
|
+
const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
6901
8087
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
6902
|
-
const title =
|
|
6903
|
-
const summary =
|
|
6904
|
-
const status =
|
|
8088
|
+
const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
8089
|
+
const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
8090
|
+
const status = text4(raw2.status, `${path}.status`, errors) ?? "";
|
|
6905
8091
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
6906
|
-
const revision =
|
|
6907
|
-
const scope = scopeValue(
|
|
6908
|
-
const tags = Array.isArray(
|
|
6909
|
-
if (!Array.isArray(
|
|
8092
|
+
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
8093
|
+
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
8094
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
8095
|
+
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
6910
8096
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
6911
8097
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
6912
|
-
const bodyMarkdown =
|
|
6913
|
-
const url =
|
|
8098
|
+
const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
8099
|
+
const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
|
|
6914
8100
|
if (url) {
|
|
6915
8101
|
try {
|
|
6916
8102
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -6918,13 +8104,13 @@ function resourceValue(value, path, errors) {
|
|
|
6918
8104
|
errors.push(`${path}.url must be a valid https URL`);
|
|
6919
8105
|
}
|
|
6920
8106
|
}
|
|
6921
|
-
const asset =
|
|
6922
|
-
if (
|
|
8107
|
+
const asset = raw2.brandAsset === void 0 ? null : record3(raw2.brandAsset);
|
|
8108
|
+
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
6923
8109
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
6924
8110
|
const brandAsset = asset ? {
|
|
6925
|
-
bookId:
|
|
6926
|
-
assetId:
|
|
6927
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
8111
|
+
bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
8112
|
+
assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
8113
|
+
...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
6928
8114
|
} : void 0;
|
|
6929
8115
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
6930
8116
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -6932,58 +8118,58 @@ function resourceValue(value, path, errors) {
|
|
|
6932
8118
|
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
6933
8119
|
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
6934
8120
|
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
6935
|
-
const provenanceRaw = record3(
|
|
8121
|
+
const provenanceRaw = record3(raw2.provenance);
|
|
6936
8122
|
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
6937
8123
|
if (provenanceRaw) unknownKeys3(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
6938
8124
|
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record3(provenanceRaw.promotedFrom);
|
|
6939
8125
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
6940
8126
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
6941
8127
|
const promotedFrom = promotedRaw ? {
|
|
6942
|
-
chapterId:
|
|
6943
|
-
resourceId:
|
|
6944
|
-
resourceRevision:
|
|
6945
|
-
documentRevision:
|
|
6946
|
-
promotedBy:
|
|
6947
|
-
promotedAt:
|
|
8128
|
+
chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
8129
|
+
resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
8130
|
+
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
8131
|
+
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
8132
|
+
promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
8133
|
+
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
6948
8134
|
} : void 0;
|
|
6949
8135
|
const provenance = {
|
|
6950
|
-
createdBy:
|
|
6951
|
-
createdAt:
|
|
6952
|
-
updatedBy:
|
|
6953
|
-
updatedAt:
|
|
8136
|
+
createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
8137
|
+
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
8138
|
+
updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
8139
|
+
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
6954
8140
|
...promotedFrom ? { promotedFrom } : {}
|
|
6955
8141
|
};
|
|
6956
8142
|
return { id, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
6957
8143
|
}
|
|
6958
8144
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
6959
8145
|
const errors = [];
|
|
6960
|
-
const
|
|
6961
|
-
if (!
|
|
6962
|
-
unknownKeys3(
|
|
6963
|
-
if (
|
|
6964
|
-
const revision =
|
|
6965
|
-
const generatedAt =
|
|
6966
|
-
const sourceRaw = record3(
|
|
8146
|
+
const raw2 = record3(value);
|
|
8147
|
+
if (!raw2) return { ok: false, errors: ["document must be an object"] };
|
|
8148
|
+
unknownKeys3(raw2, ROOT_KEYS4, "document", errors);
|
|
8149
|
+
if (raw2.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
8150
|
+
const revision = integer3(raw2.revision, "document.revision", errors);
|
|
8151
|
+
const generatedAt = integer3(raw2.generatedAt, "document.generatedAt", errors, 0);
|
|
8152
|
+
const sourceRaw = record3(raw2.source);
|
|
6967
8153
|
if (!sourceRaw) errors.push("document.source must be an object");
|
|
6968
8154
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
6969
8155
|
const sourceType = sourceRaw?.type;
|
|
6970
8156
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
6971
|
-
const sourceAppId =
|
|
8157
|
+
const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
6972
8158
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6973
8159
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
6974
8160
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
6975
|
-
const targetRaw =
|
|
6976
|
-
if (
|
|
8161
|
+
const targetRaw = raw2.target === void 0 ? null : record3(raw2.target);
|
|
8162
|
+
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
6977
8163
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
6978
8164
|
const target = targetRaw ? {
|
|
6979
|
-
appId:
|
|
6980
|
-
environment:
|
|
8165
|
+
appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
8166
|
+
environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
6981
8167
|
} : void 0;
|
|
6982
8168
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
6983
|
-
if (!Array.isArray(
|
|
6984
|
-
const inputs = Array.isArray(
|
|
8169
|
+
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
8170
|
+
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
6985
8171
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
6986
|
-
const resources = inputs.map((item,
|
|
8172
|
+
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
6987
8173
|
const ids = resources.map(({ id }) => id);
|
|
6988
8174
|
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
6989
8175
|
for (const resource of resources) {
|
|
@@ -7057,28 +8243,28 @@ function canonicalBuilderLibrary(value) {
|
|
|
7057
8243
|
return canonicalValue3(value);
|
|
7058
8244
|
}
|
|
7059
8245
|
async function builderLibraryDigest(document) {
|
|
7060
|
-
const
|
|
7061
|
-
return `sha256:${Array.from(new Uint8Array(
|
|
8246
|
+
const digest2 = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalBuilderLibrary(document)));
|
|
8247
|
+
return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
7062
8248
|
}
|
|
7063
8249
|
|
|
7064
8250
|
// src/builder-library-store.ts
|
|
7065
8251
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7066
8252
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7067
8253
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
7068
|
-
const
|
|
7069
|
-
return Array.isArray(
|
|
8254
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
8255
|
+
return Array.isArray(rows6) ? rows6.map(control2).filter(Boolean) : [];
|
|
7070
8256
|
}
|
|
7071
8257
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
7072
|
-
const
|
|
7073
|
-
return control2(Array.isArray(
|
|
8258
|
+
const rows6 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
8259
|
+
return control2(Array.isArray(rows6) ? [...rows6].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7074
8260
|
}
|
|
7075
8261
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
7076
8262
|
const id = `builder-library-local:${environment}`;
|
|
7077
|
-
const
|
|
7078
|
-
return local2(Array.isArray(
|
|
8263
|
+
const rows6 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
8264
|
+
return local2(Array.isArray(rows6) ? rows6[0] : void 0);
|
|
7079
8265
|
}
|
|
7080
8266
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
7081
|
-
const { document, digest } = envelope;
|
|
8267
|
+
const { document, digest: digest2 } = envelope;
|
|
7082
8268
|
const target = document.target;
|
|
7083
8269
|
const id = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
7084
8270
|
const ops = [];
|
|
@@ -7089,12 +8275,12 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
7089
8275
|
targetId: target.appId,
|
|
7090
8276
|
environment: target.environment,
|
|
7091
8277
|
revision: document.revision,
|
|
7092
|
-
digest,
|
|
8278
|
+
digest: digest2,
|
|
7093
8279
|
document,
|
|
7094
8280
|
active: true,
|
|
7095
8281
|
receivedAt
|
|
7096
8282
|
} });
|
|
7097
|
-
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${
|
|
8283
|
+
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest2}` });
|
|
7098
8284
|
}
|
|
7099
8285
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7100
8286
|
const id = `builder-library-local:${environment}`;
|
|
@@ -7147,7 +8333,7 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
|
7147
8333
|
} catch {
|
|
7148
8334
|
return json({ ok: false, reason: "invalid_document", errors: ["request body must be JSON"] }, 400);
|
|
7149
8335
|
}
|
|
7150
|
-
const
|
|
8336
|
+
const digest2 = typeof input.digest === "string" ? input.digest : "";
|
|
7151
8337
|
const sourceType = ctx.chapter.mode === "hub" ? "chapter" : "built_not_found";
|
|
7152
8338
|
const checked = validateBuilderLibraryDocument(input.document, {
|
|
7153
8339
|
sourceType,
|
|
@@ -7161,29 +8347,29 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
|
7161
8347
|
return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global or this Chapter's resources"] }, 400);
|
|
7162
8348
|
}
|
|
7163
8349
|
if (!document.target || document.target.environment !== env.ODLA_ENV) return json({ ok: false, reason: "wrong_environment" }, 409);
|
|
7164
|
-
if (!
|
|
8350
|
+
if (!digest2 || await builderLibraryDigest(document) !== digest2) return json({ ok: false, reason: "invalid_digest" }, 400);
|
|
7165
8351
|
const latest = await loadLatestBuilderLibraryControl(db, document.source.appId, ctx.chapter.id, env.ODLA_ENV);
|
|
7166
8352
|
if (latest && document.revision < latest.revision) return json({ ok: false, reason: "stale_revision" }, 409);
|
|
7167
8353
|
if (latest && document.revision === latest.revision) {
|
|
7168
|
-
if (latest.digest !==
|
|
8354
|
+
if (latest.digest !== digest2) return json({ ok: false, reason: "conflicting_revision" }, 409);
|
|
7169
8355
|
return json({ ok: true, target: document.target, revision: latest.revision, digest: latest.digest, status: "replayed", acknowledgedAt: latest.receivedAt });
|
|
7170
8356
|
}
|
|
7171
8357
|
const receivedAt = Date.now();
|
|
7172
8358
|
try {
|
|
7173
|
-
await materializeBuilderLibraryControl(db, { document, digest }, latest, receivedAt);
|
|
8359
|
+
await materializeBuilderLibraryControl(db, { document, digest: digest2 }, latest, receivedAt);
|
|
7174
8360
|
} catch {
|
|
7175
8361
|
return json({ ok: false, reason: "materialization_failed" }, 503);
|
|
7176
8362
|
}
|
|
7177
|
-
return json({ ok: true, target: document.target, revision: document.revision, digest, status: "accepted", acknowledgedAt: receivedAt });
|
|
8363
|
+
return json({ ok: true, target: document.target, revision: document.revision, digest: digest2, status: "accepted", acknowledgedAt: receivedAt });
|
|
7178
8364
|
};
|
|
7179
8365
|
function actorFor(user) {
|
|
7180
8366
|
return user.email ?? user.userId;
|
|
7181
8367
|
}
|
|
7182
8368
|
function withLocalAuthority2(value, mode, appId, revision, now, actor, current) {
|
|
7183
8369
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
7184
|
-
const
|
|
8370
|
+
const raw2 = value;
|
|
7185
8371
|
const currentById = new Map((current?.resources ?? []).map((item) => [item.id, item]));
|
|
7186
|
-
const resources = Array.isArray(
|
|
8372
|
+
const resources = Array.isArray(raw2.resources) ? raw2.resources.map((input) => {
|
|
7187
8373
|
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
7188
8374
|
const item = input;
|
|
7189
8375
|
const existing = typeof item.id === "string" ? currentById.get(item.id) : void 0;
|
|
@@ -7198,9 +8384,9 @@ function withLocalAuthority2(value, mode, appId, revision, now, actor, current)
|
|
|
7198
8384
|
...existing?.provenance.promotedFrom ? { promotedFrom: existing.provenance.promotedFrom } : {}
|
|
7199
8385
|
}
|
|
7200
8386
|
};
|
|
7201
|
-
}) :
|
|
8387
|
+
}) : raw2.resources;
|
|
7202
8388
|
return {
|
|
7203
|
-
...
|
|
8389
|
+
...raw2,
|
|
7204
8390
|
schemaVersion: 1,
|
|
7205
8391
|
revision,
|
|
7206
8392
|
generatedAt: now,
|
|
@@ -7281,7 +8467,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
7281
8467
|
return json({
|
|
7282
8468
|
mode: ctx.chapter.mode,
|
|
7283
8469
|
local: local3 ? { revision: local3.revision, document: local3.document } : null,
|
|
7284
|
-
sources: controls.map(({ sourceId, revision, digest, document, receivedAt }) => ({ sourceId, revision, digest, document, receivedAt })),
|
|
8470
|
+
sources: controls.map(({ sourceId, revision, digest: digest2, document, receivedAt }) => ({ sourceId, revision, digest: digest2, document, receivedAt })),
|
|
7285
8471
|
localAuthoring: ctx.chapter.builderLibrary.localAuthoring,
|
|
7286
8472
|
chapterTargets: ctx.chapter.mode === "hub" ? ctx.chapter.builderLibrary.sources.map((source) => ({ chapterId: source.sourceId })) : [{ chapterId: ctx.chapter.id }],
|
|
7287
8473
|
visibility: { builtNotFoundSeesChapterSources: ctx.chapter.mode === "hub", chaptersSeeSiblingSources: false }
|
|
@@ -7289,11 +8475,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
7289
8475
|
}
|
|
7290
8476
|
if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
|
|
7291
8477
|
if (!ctx.chapter.builderLibrary.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
|
|
7292
|
-
const
|
|
7293
|
-
if (
|
|
8478
|
+
const raw2 = await req.text();
|
|
8479
|
+
if (raw2.length > BODY_CAP2) return json({ error: "request body too large" }, 413);
|
|
7294
8480
|
let input;
|
|
7295
8481
|
try {
|
|
7296
|
-
input = JSON.parse(
|
|
8482
|
+
input = JSON.parse(raw2);
|
|
7297
8483
|
} catch {
|
|
7298
8484
|
return json({ error: "invalid JSON body" }, 400);
|
|
7299
8485
|
}
|
|
@@ -7398,14 +8584,14 @@ function validatedTokens(input) {
|
|
|
7398
8584
|
}
|
|
7399
8585
|
const tokens = {};
|
|
7400
8586
|
let serializedLength = 0;
|
|
7401
|
-
for (const [name,
|
|
8587
|
+
for (const [name, raw2] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
7402
8588
|
if (!TOKEN_NAME.test(name)) {
|
|
7403
8589
|
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
7404
8590
|
}
|
|
7405
|
-
if (typeof
|
|
8591
|
+
if (typeof raw2 !== "string" && typeof raw2 !== "number" || typeof raw2 === "number" && !Number.isFinite(raw2)) {
|
|
7406
8592
|
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
7407
8593
|
}
|
|
7408
|
-
const value = String(
|
|
8594
|
+
const value = String(raw2).trim();
|
|
7409
8595
|
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
7410
8596
|
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
7411
8597
|
}
|
|
@@ -7527,6 +8713,7 @@ var BUILTIN_ROUTES = [
|
|
|
7527
8713
|
handleNetworkSnapshot,
|
|
7528
8714
|
handleNetworkRecords,
|
|
7529
8715
|
handleNetworkSharedNotes,
|
|
8716
|
+
handleNetworkCommercialStripeWebhook,
|
|
7530
8717
|
handleFormation,
|
|
7531
8718
|
handleMember,
|
|
7532
8719
|
handleMemberContent,
|
|
@@ -7566,6 +8753,8 @@ var BUILTIN_ROUTES = [
|
|
|
7566
8753
|
handleAdminNetworkRecords,
|
|
7567
8754
|
handleAdminNetworkNotes,
|
|
7568
8755
|
handleAdminNetworkSharedNotes,
|
|
8756
|
+
handleAdminNetworkPersonRegistry,
|
|
8757
|
+
handleAdminNetworkCommercialReadModel,
|
|
7569
8758
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
7570
8759
|
// get first refusal through options.routes, then this terminates unknown API
|
|
7571
8760
|
// paths with an explicit machine-readable 404.
|