@odla-ai/chapter 0.47.3 → 0.47.5
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/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +321 -173
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +321 -173
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -340,8 +340,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
340
340
|
function byOrder(a, b) {
|
|
341
341
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
342
342
|
}
|
|
343
|
-
function resolveTiers(group,
|
|
344
|
-
const stored =
|
|
343
|
+
function resolveTiers(group, rows5 = []) {
|
|
344
|
+
const stored = rows5.filter((tier) => tier.active);
|
|
345
345
|
if (stored.length) return [...stored].sort(byOrder);
|
|
346
346
|
return [{
|
|
347
347
|
id: "standard",
|
|
@@ -387,15 +387,15 @@ function rowToTier(row, groupId) {
|
|
|
387
387
|
}
|
|
388
388
|
async function loadTiers(db, group) {
|
|
389
389
|
const groupId = String(group.id ?? "");
|
|
390
|
-
let
|
|
390
|
+
let rows5;
|
|
391
391
|
try {
|
|
392
|
-
|
|
392
|
+
rows5 = (await db.query({
|
|
393
393
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
394
394
|
})).tiers;
|
|
395
395
|
} catch {
|
|
396
|
-
|
|
396
|
+
rows5 = void 0;
|
|
397
397
|
}
|
|
398
|
-
const list = Array.isArray(
|
|
398
|
+
const list = Array.isArray(rows5) ? rows5.map((row) => rowToTier(row, groupId)) : [];
|
|
399
399
|
return resolveTiers(group, list);
|
|
400
400
|
}
|
|
401
401
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -1454,27 +1454,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1454
1454
|
if (db.aggregate) {
|
|
1455
1455
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1456
1456
|
}
|
|
1457
|
-
const
|
|
1458
|
-
return Array.isArray(
|
|
1457
|
+
const rows5 = (await db.query({ signupControls: {} })).signupControls;
|
|
1458
|
+
return Array.isArray(rows5) && rows5.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1459
1459
|
}
|
|
1460
1460
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1461
1461
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1462
|
-
const
|
|
1462
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1463
1463
|
where: { targetId, environment },
|
|
1464
1464
|
order: { revision: "desc" },
|
|
1465
1465
|
limit: 1
|
|
1466
1466
|
} } })).signupControls;
|
|
1467
|
-
const latest = Array.isArray(
|
|
1467
|
+
const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1468
1468
|
return asStored(latest);
|
|
1469
1469
|
}
|
|
1470
1470
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1471
1471
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1472
|
-
const
|
|
1472
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1473
1473
|
where: { targetId, environment, active: true },
|
|
1474
1474
|
order: { revision: "desc" },
|
|
1475
1475
|
limit: 1
|
|
1476
1476
|
} } })).signupControls;
|
|
1477
|
-
const active = Array.isArray(
|
|
1477
|
+
const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1478
1478
|
return asStored(active);
|
|
1479
1479
|
}
|
|
1480
1480
|
function controlledApplication(base, document) {
|
|
@@ -2850,33 +2850,33 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2850
2850
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2851
2851
|
async function exists(db, ns, where) {
|
|
2852
2852
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2853
|
-
const
|
|
2854
|
-
return
|
|
2853
|
+
const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2854
|
+
return rows5.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2855
2855
|
}
|
|
2856
2856
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2857
2857
|
const where = { targetId, environment, active: true };
|
|
2858
2858
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2859
|
-
const
|
|
2859
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2860
2860
|
where,
|
|
2861
2861
|
order: { revision: "desc" },
|
|
2862
2862
|
limit: 1
|
|
2863
2863
|
} } })).memberContentControls;
|
|
2864
|
-
return control(Array.isArray(
|
|
2864
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2865
2865
|
}
|
|
2866
2866
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2867
2867
|
const where = { targetId, environment };
|
|
2868
2868
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2869
|
-
const
|
|
2869
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2870
2870
|
where,
|
|
2871
2871
|
order: { revision: "desc" },
|
|
2872
2872
|
limit: 1
|
|
2873
2873
|
} } })).memberContentControls;
|
|
2874
|
-
return control(Array.isArray(
|
|
2874
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2875
2875
|
}
|
|
2876
2876
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2877
2877
|
const id = `member-content-local:${environment}`;
|
|
2878
|
-
const
|
|
2879
|
-
return local(Array.isArray(
|
|
2878
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2879
|
+
return local(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
2880
2880
|
}
|
|
2881
2881
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2882
2882
|
const { document, digest: digest2 } = envelope;
|
|
@@ -2931,12 +2931,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2931
2931
|
}
|
|
2932
2932
|
async function memberTierId(db, chapterId, email) {
|
|
2933
2933
|
if (!email) return null;
|
|
2934
|
-
let
|
|
2934
|
+
let rows5 = (await db.query({ applications: { $: {
|
|
2935
2935
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2936
2936
|
limit: 20
|
|
2937
2937
|
} } })).applications;
|
|
2938
|
-
if (!
|
|
2939
|
-
const latest = Array.isArray(
|
|
2938
|
+
if (!rows5?.length) rows5 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2939
|
+
const latest = Array.isArray(rows5) ? [...rows5].filter((row) => String(row.groupId ?? chapterId) === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
|
|
2940
2940
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2941
2941
|
}
|
|
2942
2942
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -3662,8 +3662,8 @@ function makeCalendar(env) {
|
|
|
3662
3662
|
}
|
|
3663
3663
|
async function firstRow(db, ns, q) {
|
|
3664
3664
|
const res = await db.query({ [ns]: { $: q } });
|
|
3665
|
-
const
|
|
3666
|
-
return Array.isArray(
|
|
3665
|
+
const rows5 = res[ns];
|
|
3666
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
3667
3667
|
}
|
|
3668
3668
|
async function computeSlots(cal, cfg) {
|
|
3669
3669
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -4063,8 +4063,8 @@ function createStripeProvider(config) {
|
|
|
4063
4063
|
const sk = requireSecret(secretKey);
|
|
4064
4064
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4065
4065
|
if (!charges.ok) fail("charges list", charges);
|
|
4066
|
-
const
|
|
4067
|
-
const paid =
|
|
4066
|
+
const rows5 = charges.body.data ?? [];
|
|
4067
|
+
const paid = rows5.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4068
4068
|
const charge = paid[paid.length - 1];
|
|
4069
4069
|
if (!charge) {
|
|
4070
4070
|
const err = new Error("no paid charge to refund");
|
|
@@ -4172,8 +4172,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4172
4172
|
throw new Error("gift issuance conflict");
|
|
4173
4173
|
}
|
|
4174
4174
|
}
|
|
4175
|
-
function buyerGiftClaims(
|
|
4176
|
-
return
|
|
4175
|
+
function buyerGiftClaims(rows5, now) {
|
|
4176
|
+
return rows5.map((row) => {
|
|
4177
4177
|
const stored = String(row.status ?? "pending");
|
|
4178
4178
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4179
4179
|
return {
|
|
@@ -4465,8 +4465,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4465
4465
|
|
|
4466
4466
|
// src/stripe-webhook-partition.ts
|
|
4467
4467
|
var firstRow2 = async (db, ns, q) => {
|
|
4468
|
-
const
|
|
4469
|
-
return Array.isArray(
|
|
4468
|
+
const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4469
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4470
4470
|
};
|
|
4471
4471
|
var stripeEventDigest = async (rawBody) => {
|
|
4472
4472
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4536,8 +4536,8 @@ async function replayedStripeReceipt(db, eventId, digest2, event) {
|
|
|
4536
4536
|
// src/worker-routes-payments.ts
|
|
4537
4537
|
async function firstRow3(db, ns, q) {
|
|
4538
4538
|
const res = await db.query({ [ns]: { $: q } });
|
|
4539
|
-
const
|
|
4540
|
-
return Array.isArray(
|
|
4539
|
+
const rows5 = res[ns];
|
|
4540
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4541
4541
|
}
|
|
4542
4542
|
async function findApplication(db, event) {
|
|
4543
4543
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -5003,8 +5003,8 @@ async function upcomingEvents(env) {
|
|
|
5003
5003
|
const res = await cal.availability.upcoming();
|
|
5004
5004
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5005
5005
|
}
|
|
5006
|
-
function toReconcile(
|
|
5007
|
-
return
|
|
5006
|
+
function toReconcile(rows5) {
|
|
5007
|
+
return rows5.map((m) => ({
|
|
5008
5008
|
id: String(m.id),
|
|
5009
5009
|
applicationId: String(m.applicationId),
|
|
5010
5010
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5026,12 +5026,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5026
5026
|
const query = await db.query({
|
|
5027
5027
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5028
5028
|
});
|
|
5029
|
-
let
|
|
5030
|
-
if (Number.isFinite(from))
|
|
5031
|
-
if (Number.isFinite(to))
|
|
5029
|
+
let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5030
|
+
if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5031
|
+
if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5032
5032
|
let decisions = [];
|
|
5033
5033
|
try {
|
|
5034
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5034
|
+
decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
|
|
5035
5035
|
} catch {
|
|
5036
5036
|
}
|
|
5037
5037
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5058,7 +5058,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5058
5058
|
};
|
|
5059
5059
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5060
5060
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5061
|
-
const meetings =
|
|
5061
|
+
const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5062
5062
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5063
5063
|
};
|
|
5064
5064
|
|
|
@@ -5134,10 +5134,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5134
5134
|
});
|
|
5135
5135
|
}
|
|
5136
5136
|
}
|
|
5137
|
-
const
|
|
5137
|
+
const rows5 = [...people.values()].sort(
|
|
5138
5138
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5139
5139
|
);
|
|
5140
|
-
return json({ people:
|
|
5140
|
+
return json({ people: rows5 });
|
|
5141
5141
|
};
|
|
5142
5142
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5143
5143
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
@@ -6993,6 +6993,9 @@ async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
|
6993
6993
|
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6994
6994
|
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6995
6995
|
}
|
|
6996
|
+
async function networkChapterPersonSourceKey(chapterId, chapterPersonId) {
|
|
6997
|
+
return `chapter_source_${await sha256(`${chapterId}\0${chapterPersonId}`)}`;
|
|
6998
|
+
}
|
|
6996
6999
|
function projectNetworkContactPolicy(policy, links) {
|
|
6997
7000
|
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6998
7001
|
}
|
|
@@ -7010,7 +7013,7 @@ function canonicalNetworkPersonId(personId, persons) {
|
|
|
7010
7013
|
}
|
|
7011
7014
|
}
|
|
7012
7015
|
|
|
7013
|
-
// src/network-person-store.ts
|
|
7016
|
+
// src/network-person-store-state.ts
|
|
7014
7017
|
var NetworkPersonRegistryError = class extends Error {
|
|
7015
7018
|
constructor(code, message) {
|
|
7016
7019
|
super(message);
|
|
@@ -7018,29 +7021,112 @@ var NetworkPersonRegistryError = class extends Error {
|
|
|
7018
7021
|
}
|
|
7019
7022
|
code;
|
|
7020
7023
|
};
|
|
7021
|
-
var
|
|
7022
|
-
async function loadNetworkPersonRegistry(db) {
|
|
7023
|
-
const result = await db.query({
|
|
7024
|
-
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
7025
|
-
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
7026
|
-
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
7027
|
-
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
7028
|
-
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
7029
|
-
});
|
|
7030
|
-
return {
|
|
7031
|
-
persons: rows4(result.networkPersons),
|
|
7032
|
-
claims: rows4(result.networkIdentityClaims),
|
|
7033
|
-
aliases: rows4(result.networkPersonAliases),
|
|
7034
|
-
links: rows4(result.networkChapterPersonLinks),
|
|
7035
|
-
events: rows4(result.networkPersonEvents)
|
|
7036
|
-
};
|
|
7037
|
-
}
|
|
7038
|
-
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7024
|
+
var networkPersonEventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7039
7025
|
t: "update",
|
|
7040
7026
|
ns: "networkPersonEvents",
|
|
7041
7027
|
id: `network_person_event:${mutationId}`,
|
|
7042
7028
|
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
7043
7029
|
});
|
|
7030
|
+
var asRows = (value) => Array.isArray(value) ? value : [];
|
|
7031
|
+
var MAX_ROWS = 1e4;
|
|
7032
|
+
async function boundedRows(db, ns, order2) {
|
|
7033
|
+
const collected = [];
|
|
7034
|
+
const pageSize = 500;
|
|
7035
|
+
for (let offset = 0; offset <= MAX_ROWS; offset += pageSize) {
|
|
7036
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset, ...order2 ? { order: order2 } : {} } } });
|
|
7037
|
+
const page = asRows(result[ns]);
|
|
7038
|
+
collected.push(...page);
|
|
7039
|
+
if (collected.length > MAX_ROWS) {
|
|
7040
|
+
throw new NetworkPersonRegistryError(
|
|
7041
|
+
"registry_too_large",
|
|
7042
|
+
`network Person registry ${ns} exceeds the ${MAX_ROWS.toLocaleString()}-row operational bound`
|
|
7043
|
+
);
|
|
7044
|
+
}
|
|
7045
|
+
if (page.length < pageSize) return collected;
|
|
7046
|
+
}
|
|
7047
|
+
return collected;
|
|
7048
|
+
}
|
|
7049
|
+
async function loadNetworkPersonRegistry(db) {
|
|
7050
|
+
const [persons, claims, aliases, links, events] = await Promise.all([
|
|
7051
|
+
boundedRows(db, "networkPersons", { updatedAt: "desc" }),
|
|
7052
|
+
boundedRows(db, "networkIdentityClaims"),
|
|
7053
|
+
boundedRows(db, "networkPersonAliases", { updatedAt: "desc" }),
|
|
7054
|
+
boundedRows(db, "networkChapterPersonLinks"),
|
|
7055
|
+
boundedRows(db, "networkPersonEvents", { createdAt: "desc" })
|
|
7056
|
+
]);
|
|
7057
|
+
return { persons, claims, aliases, links, events };
|
|
7058
|
+
}
|
|
7059
|
+
|
|
7060
|
+
// src/network-person-link-store.ts
|
|
7061
|
+
async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7062
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7063
|
+
const canonicalId = canonicalNetworkPersonId(input.personId, state.persons);
|
|
7064
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7065
|
+
if (!person || person.state !== "active" || canonicalId !== person.id) {
|
|
7066
|
+
throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
7067
|
+
}
|
|
7068
|
+
const sourceKey = await networkChapterPersonSourceKey(input.chapterId, input.chapterPersonId);
|
|
7069
|
+
const existingSource = state.links.find((link2) => link2.state === "shadow" && (link2.sourceKey === sourceKey || link2.chapterId === input.chapterId && link2.chapterPersonId === input.chapterPersonId));
|
|
7070
|
+
if (existingSource) {
|
|
7071
|
+
if (existingSource.personId !== person.id) {
|
|
7072
|
+
throw new NetworkPersonRegistryError("source_already_linked", "chapter source row is linked to another Person");
|
|
7073
|
+
}
|
|
7074
|
+
return { person, link: existingSource, replayed: true };
|
|
7075
|
+
}
|
|
7076
|
+
if (person.revision !== input.expectedPersonRevision) {
|
|
7077
|
+
throw new NetworkPersonRegistryError("stale_source_link", "Person changed; reload before linking this source row");
|
|
7078
|
+
}
|
|
7079
|
+
const expectedClaim = await networkIdentityClaim({ kind: "email", value: input.verifiedEmail, verifiedAt: 1 });
|
|
7080
|
+
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
7081
|
+
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
7082
|
+
}
|
|
7083
|
+
const id = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
7084
|
+
const previous = state.links.find((link2) => link2.id === id);
|
|
7085
|
+
const link = {
|
|
7086
|
+
id,
|
|
7087
|
+
personId: person.id,
|
|
7088
|
+
chapterId: input.chapterId,
|
|
7089
|
+
chapterPersonId: input.chapterPersonId,
|
|
7090
|
+
sourceKey,
|
|
7091
|
+
state: "shadow",
|
|
7092
|
+
createdAt: previous?.createdAt ?? now
|
|
7093
|
+
};
|
|
7094
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7095
|
+
await db.transact([
|
|
7096
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7097
|
+
{ t: "update", ns: "networkChapterPersonLinks", id, attrs: { ...link } },
|
|
7098
|
+
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
7099
|
+
], { mutationId, guards: [
|
|
7100
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7101
|
+
{ ns: "networkChapterPersonLinks", id, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
7102
|
+
] });
|
|
7103
|
+
return { person: nextPerson, link };
|
|
7104
|
+
}
|
|
7105
|
+
async function unlinkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7106
|
+
const reason = input.reason.trim();
|
|
7107
|
+
if (!reason || reason.length > 500) throw new NetworkPersonRegistryError("invalid_unlink", "a bounded unlink reason is required");
|
|
7108
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7109
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7110
|
+
const link = state.links.find((row) => row.id === input.linkId && row.personId === input.personId);
|
|
7111
|
+
if (!person || person.state !== "active" || person.revision !== input.expectedPersonRevision || !link || link.state !== "shadow") {
|
|
7112
|
+
throw new NetworkPersonRegistryError("stale_source_unlink", "Person or source link changed; reload before undoing the link");
|
|
7113
|
+
}
|
|
7114
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7115
|
+
const nextLink = { ...link, state: "revoked" };
|
|
7116
|
+
delete nextLink.sourceKey;
|
|
7117
|
+
await db.transact([
|
|
7118
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7119
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: link.id, attrs: { state: "revoked" } },
|
|
7120
|
+
{ t: "retract", ns: "networkChapterPersonLinks", id: link.id, attrs: ["sourceKey"] },
|
|
7121
|
+
networkPersonEventOp("chapter_source_unlinked", mutationId, actor, now, { chapterId: link.chapterId, reason }, { personId: person.id })
|
|
7122
|
+
], { mutationId, guards: [
|
|
7123
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7124
|
+
{ ns: "networkChapterPersonLinks", id: link.id, exists: true, equals: { state: "shadow", personId: person.id } }
|
|
7125
|
+
] });
|
|
7126
|
+
return { person: nextPerson, link: nextLink };
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
// src/network-person-store.ts
|
|
7044
7130
|
var defaultPolicy = (now, consent) => ({
|
|
7045
7131
|
marketingSuppressed: consent.marketing !== "granted",
|
|
7046
7132
|
discretionarySuppressed: consent.discretionary !== "granted",
|
|
@@ -7050,7 +7136,8 @@ var isGuardConflict = (error) => error instanceof Error && error.code === "trans
|
|
|
7050
7136
|
async function linkOps(personId, links, now) {
|
|
7051
7137
|
return Promise.all(links.map(async (link) => {
|
|
7052
7138
|
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
7053
|
-
|
|
7139
|
+
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
7140
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
7054
7141
|
}));
|
|
7055
7142
|
}
|
|
7056
7143
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -7072,7 +7159,7 @@ async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId
|
|
|
7072
7159
|
};
|
|
7073
7160
|
await db.transact([
|
|
7074
7161
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
7075
|
-
|
|
7162
|
+
networkPersonEventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
7076
7163
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
7077
7164
|
return { status: "ambiguous", alias };
|
|
7078
7165
|
}
|
|
@@ -7108,7 +7195,7 @@ async function importNetworkPerson(db, input, mutationId, actor, now = Date.now(
|
|
|
7108
7195
|
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
7109
7196
|
...claimOps,
|
|
7110
7197
|
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
7111
|
-
|
|
7198
|
+
networkPersonEventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
7112
7199
|
];
|
|
7113
7200
|
try {
|
|
7114
7201
|
await db.transact(ops, { mutationId, guards: [
|
|
@@ -7130,12 +7217,21 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7130
7217
|
const state = await loadNetworkPersonRegistry(db);
|
|
7131
7218
|
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
7132
7219
|
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
7133
|
-
|
|
7220
|
+
const expectedStatus = input.decision === "reopen" ? "excluded" : "ambiguous";
|
|
7221
|
+
if (alias.revision !== input.expectedRevision || alias.status !== expectedStatus) throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
7222
|
+
if (input.decision === "reopen") {
|
|
7223
|
+
const next2 = { ...alias, status: "ambiguous", revision: alias.revision + 1, updatedAt: now };
|
|
7224
|
+
await db.transact([
|
|
7225
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7226
|
+
networkPersonEventOp("alias_reopened", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7227
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "excluded" } }] });
|
|
7228
|
+
return next2;
|
|
7229
|
+
}
|
|
7134
7230
|
if (input.decision === "exclude") {
|
|
7135
7231
|
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
7136
7232
|
await db.transact([
|
|
7137
7233
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7138
|
-
|
|
7234
|
+
networkPersonEventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7139
7235
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
7140
7236
|
return next2;
|
|
7141
7237
|
}
|
|
@@ -7151,7 +7247,7 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7151
7247
|
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7152
7248
|
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7153
7249
|
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7154
|
-
|
|
7250
|
+
networkPersonEventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7155
7251
|
], { mutationId, guards: [
|
|
7156
7252
|
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7157
7253
|
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
@@ -7181,7 +7277,7 @@ async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now(
|
|
|
7181
7277
|
await db.transact([
|
|
7182
7278
|
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7183
7279
|
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7184
|
-
|
|
7280
|
+
networkPersonEventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7185
7281
|
], { mutationId, guards: [
|
|
7186
7282
|
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7187
7283
|
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
@@ -7197,7 +7293,7 @@ async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now
|
|
|
7197
7293
|
await db.transact([
|
|
7198
7294
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7199
7295
|
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7200
|
-
|
|
7296
|
+
networkPersonEventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7201
7297
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7202
7298
|
return next;
|
|
7203
7299
|
}
|
|
@@ -7211,63 +7307,30 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
7211
7307
|
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7212
7308
|
await db.transact([
|
|
7213
7309
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7214
|
-
|
|
7310
|
+
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7215
7311
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7216
7312
|
return next;
|
|
7217
7313
|
}
|
|
7218
7314
|
|
|
7219
|
-
// src/network-person-
|
|
7220
|
-
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7221
|
-
const reason = input.reason.trim();
|
|
7222
|
-
if (!reason || reason.length > 500) {
|
|
7223
|
-
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7224
|
-
}
|
|
7225
|
-
const state = await loadNetworkPersonRegistry(db);
|
|
7226
|
-
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7227
|
-
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7228
|
-
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7229
|
-
const event = {
|
|
7230
|
-
t: "update",
|
|
7231
|
-
ns: "networkPersonEvents",
|
|
7232
|
-
id: `network_person_event:${mutationId}`,
|
|
7233
|
-
attrs: {
|
|
7234
|
-
id: `network_person_event:${mutationId}`,
|
|
7235
|
-
kind: "pii_revealed",
|
|
7236
|
-
actorId: actor.id,
|
|
7237
|
-
...ids,
|
|
7238
|
-
mutationId,
|
|
7239
|
-
detail: { targetKind: input.kind, reason },
|
|
7240
|
-
createdAt: now
|
|
7241
|
-
}
|
|
7242
|
-
};
|
|
7243
|
-
await db.transact([event], {
|
|
7244
|
-
mutationId,
|
|
7245
|
-
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7246
|
-
});
|
|
7247
|
-
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7248
|
-
}
|
|
7249
|
-
|
|
7250
|
-
// src/worker-routes-network-person-registry.ts
|
|
7251
|
-
var PATH2 = "/api/admin/network-person-registry";
|
|
7252
|
-
var MAX_BODY2 = 32e3;
|
|
7253
|
-
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7315
|
+
// src/network-person-route-projection.ts
|
|
7254
7316
|
function redactedName(displayName) {
|
|
7255
7317
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7256
7318
|
}
|
|
7257
|
-
var
|
|
7319
|
+
var redactedNetworkPersonLink = async (link) => ({
|
|
7258
7320
|
id: link.id,
|
|
7259
7321
|
personId: link.personId,
|
|
7260
7322
|
chapterId: link.chapterId,
|
|
7323
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId),
|
|
7261
7324
|
state: link.state,
|
|
7262
7325
|
createdAt: link.createdAt
|
|
7263
7326
|
});
|
|
7264
|
-
var
|
|
7327
|
+
var safeNetworkContactPolicy = (policy) => ({
|
|
7265
7328
|
marketingSuppressed: policy.marketingSuppressed,
|
|
7266
7329
|
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7267
7330
|
hasReason: Boolean(policy.reason),
|
|
7268
7331
|
updatedAt: policy.updatedAt
|
|
7269
7332
|
});
|
|
7270
|
-
var
|
|
7333
|
+
var safeNetworkPersonAlias = async (alias) => ({
|
|
7271
7334
|
id: alias.id,
|
|
7272
7335
|
sourceSystem: alias.sourceSystem,
|
|
7273
7336
|
displayNameHint: redactedName(alias.displayName),
|
|
@@ -7282,17 +7345,91 @@ var safeAlias = (alias) => ({
|
|
|
7282
7345
|
},
|
|
7283
7346
|
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7284
7347
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7348
|
+
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
7349
|
+
chapterId: link.chapterId,
|
|
7350
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId)
|
|
7351
|
+
}))),
|
|
7285
7352
|
revision: alias.revision,
|
|
7286
7353
|
updatedAt: alias.updatedAt
|
|
7287
7354
|
});
|
|
7288
|
-
var
|
|
7355
|
+
var safeNetworkPerson = (person, claims = []) => ({
|
|
7289
7356
|
id: person.id,
|
|
7357
|
+
displayName: person.displayName,
|
|
7290
7358
|
displayNameHint: redactedName(person.displayName),
|
|
7359
|
+
verifiedEmails: claims.filter((claim) => claim.kind === "email").map((claim) => ({ value: claim.value, verifiedAt: claim.verifiedAt })).sort((left, right) => right.verifiedAt - left.verifiedAt || left.value.localeCompare(right.value)),
|
|
7291
7360
|
state: person.state,
|
|
7292
7361
|
mergedIntoId: person.mergedIntoId ?? null,
|
|
7293
7362
|
revision: person.revision,
|
|
7294
|
-
contactPolicy:
|
|
7363
|
+
contactPolicy: safeNetworkContactPolicy(person.contactPolicy)
|
|
7295
7364
|
});
|
|
7365
|
+
async function projectNetworkPersonRegistryState(state) {
|
|
7366
|
+
const persons = await Promise.all(state.persons.map(async (person) => {
|
|
7367
|
+
const claims = state.claims.filter((claim) => claim.personId === person.id);
|
|
7368
|
+
const links = state.links.filter((link) => link.personId === person.id && link.state === "shadow");
|
|
7369
|
+
const safeLinks = await Promise.all(links.map(redactedNetworkPersonLink));
|
|
7370
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7371
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7372
|
+
return {
|
|
7373
|
+
...safeNetworkPerson(person, claims),
|
|
7374
|
+
canonicalPersonId: canonicalId,
|
|
7375
|
+
identityClaims: claims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7376
|
+
chapterLinks: safeLinks,
|
|
7377
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
7378
|
+
};
|
|
7379
|
+
}));
|
|
7380
|
+
const aliases = await Promise.all(state.aliases.map(safeNetworkPersonAlias));
|
|
7381
|
+
return {
|
|
7382
|
+
persons,
|
|
7383
|
+
aliases,
|
|
7384
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7385
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7386
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7387
|
+
id: event.id,
|
|
7388
|
+
kind: event.kind,
|
|
7389
|
+
personId: event.personId ?? null,
|
|
7390
|
+
aliasId: event.aliasId ?? null,
|
|
7391
|
+
actorId: event.actorId,
|
|
7392
|
+
mutationId: event.mutationId,
|
|
7393
|
+
createdAt: event.createdAt
|
|
7394
|
+
}))
|
|
7395
|
+
};
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7398
|
+
// src/network-person-reveal.ts
|
|
7399
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7400
|
+
const reason = input.reason.trim();
|
|
7401
|
+
if (!reason || reason.length > 500) {
|
|
7402
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7403
|
+
}
|
|
7404
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7405
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7406
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7407
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7408
|
+
const event = {
|
|
7409
|
+
t: "update",
|
|
7410
|
+
ns: "networkPersonEvents",
|
|
7411
|
+
id: `network_person_event:${mutationId}`,
|
|
7412
|
+
attrs: {
|
|
7413
|
+
id: `network_person_event:${mutationId}`,
|
|
7414
|
+
kind: "pii_revealed",
|
|
7415
|
+
actorId: actor.id,
|
|
7416
|
+
...ids,
|
|
7417
|
+
mutationId,
|
|
7418
|
+
detail: { targetKind: input.kind, reason },
|
|
7419
|
+
createdAt: now
|
|
7420
|
+
}
|
|
7421
|
+
};
|
|
7422
|
+
await db.transact([event], {
|
|
7423
|
+
mutationId,
|
|
7424
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7425
|
+
});
|
|
7426
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7427
|
+
}
|
|
7428
|
+
|
|
7429
|
+
// src/worker-routes-network-person-registry.ts
|
|
7430
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7431
|
+
var MAX_BODY2 = 32e3;
|
|
7432
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7296
7433
|
var positiveRevision = (value) => {
|
|
7297
7434
|
const parsed = Number(value);
|
|
7298
7435
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
@@ -7348,14 +7485,14 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7348
7485
|
return json({
|
|
7349
7486
|
result: {
|
|
7350
7487
|
status: result.status,
|
|
7351
|
-
alias:
|
|
7352
|
-
..."person" in result && result.person ? { person:
|
|
7488
|
+
alias: await safeNetworkPersonAlias(result.alias),
|
|
7489
|
+
..."person" in result && result.person ? { person: safeNetworkPerson(result.person, result.alias.identityClaims) } : {},
|
|
7353
7490
|
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7354
7491
|
}
|
|
7355
7492
|
}, 201);
|
|
7356
7493
|
}
|
|
7357
7494
|
if (command === "review") {
|
|
7358
|
-
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or
|
|
7495
|
+
if (input.decision !== "link" && input.decision !== "exclude" && input.decision !== "reopen") return json({ error: "review decision must be link, exclude, or reopen" }, 400);
|
|
7359
7496
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7360
7497
|
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7361
7498
|
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
@@ -7365,7 +7502,42 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7365
7502
|
decision: input.decision,
|
|
7366
7503
|
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7367
7504
|
}, mutationId, actionActor);
|
|
7368
|
-
return json({ result:
|
|
7505
|
+
return json({ result: await safeNetworkPersonAlias(result) });
|
|
7506
|
+
}
|
|
7507
|
+
if (command === "link-source") {
|
|
7508
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7509
|
+
const chapterId = typeof input.chapterId === "string" ? input.chapterId : "";
|
|
7510
|
+
const chapterPersonId = typeof input.chapterPersonId === "string" ? input.chapterPersonId.trim() : "";
|
|
7511
|
+
const verifiedEmail = typeof input.verifiedEmail === "string" ? input.verifiedEmail.trim() : "";
|
|
7512
|
+
if (!expectedPersonRevision || !ctx.chapter.networkPersonRegistry.chapterIds.includes(chapterId) || !chapterPersonId || chapterPersonId.length > 200 || !verifiedEmail || verifiedEmail.length > 320) return json({ error: "a configured source row, exact email, and Person revision are required" }, 400);
|
|
7513
|
+
const result = await linkNetworkChapterPerson(db, {
|
|
7514
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7515
|
+
expectedPersonRevision,
|
|
7516
|
+
chapterId,
|
|
7517
|
+
chapterPersonId,
|
|
7518
|
+
verifiedEmail
|
|
7519
|
+
}, mutationId, actionActor);
|
|
7520
|
+
return json({
|
|
7521
|
+
result: {
|
|
7522
|
+
person: safeNetworkPerson(result.person),
|
|
7523
|
+
link: await redactedNetworkPersonLink(result.link),
|
|
7524
|
+
...result.replayed ? { replayed: true } : {}
|
|
7525
|
+
}
|
|
7526
|
+
});
|
|
7527
|
+
}
|
|
7528
|
+
if (command === "unlink-source") {
|
|
7529
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7530
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7531
|
+
if (!expectedPersonRevision || !reason || reason.length > 500) {
|
|
7532
|
+
return json({ error: "a Person revision and bounded unlink reason are required" }, 400);
|
|
7533
|
+
}
|
|
7534
|
+
const result = await unlinkNetworkChapterPerson(db, {
|
|
7535
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7536
|
+
linkId: typeof input.linkId === "string" ? input.linkId : "",
|
|
7537
|
+
expectedPersonRevision,
|
|
7538
|
+
reason
|
|
7539
|
+
}, mutationId, actionActor);
|
|
7540
|
+
return json({ result: { person: safeNetworkPerson(result.person), linkState: result.link.state } });
|
|
7369
7541
|
}
|
|
7370
7542
|
if (command === "merge") {
|
|
7371
7543
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7377,7 +7549,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7377
7549
|
expectedRevision,
|
|
7378
7550
|
expectedTargetRevision
|
|
7379
7551
|
}, mutationId, actionActor);
|
|
7380
|
-
return json({ result: { source:
|
|
7552
|
+
return json({ result: { source: safeNetworkPerson(result.source), target: safeNetworkPerson(result.target) } });
|
|
7381
7553
|
}
|
|
7382
7554
|
if (command === "unmerge") {
|
|
7383
7555
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7386,7 +7558,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7386
7558
|
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7387
7559
|
expectedRevision
|
|
7388
7560
|
}, mutationId, actionActor);
|
|
7389
|
-
return json({ result:
|
|
7561
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7390
7562
|
}
|
|
7391
7563
|
if (command === "suppression") {
|
|
7392
7564
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7402,7 +7574,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7402
7574
|
discretionarySuppressed: input.discretionarySuppressed,
|
|
7403
7575
|
...reason ? { reason } : {}
|
|
7404
7576
|
}, mutationId, actionActor);
|
|
7405
|
-
return json({ result:
|
|
7577
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7406
7578
|
}
|
|
7407
7579
|
if (command === "reveal") {
|
|
7408
7580
|
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
@@ -7421,43 +7593,19 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7421
7593
|
}
|
|
7422
7594
|
}
|
|
7423
7595
|
const state = await loadNetworkPersonRegistry(db);
|
|
7424
|
-
const
|
|
7425
|
-
const links = state.links.filter((link) => link.personId === person.id);
|
|
7426
|
-
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7427
|
-
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7428
|
-
return {
|
|
7429
|
-
id: person.id,
|
|
7430
|
-
displayNameHint: redactedName(person.displayName),
|
|
7431
|
-
state: person.state,
|
|
7432
|
-
canonicalPersonId: canonicalId,
|
|
7433
|
-
mergedIntoId: person.mergedIntoId ?? null,
|
|
7434
|
-
revision: person.revision,
|
|
7435
|
-
contactPolicy: safePolicy(person.contactPolicy),
|
|
7436
|
-
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7437
|
-
chapterLinks: links.map(redactedLink),
|
|
7438
|
-
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, links.map(redactedLink)).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7439
|
-
};
|
|
7440
|
-
});
|
|
7441
|
-
const aliases = state.aliases.map(safeAlias);
|
|
7596
|
+
const projection = await projectNetworkPersonRegistryState(state);
|
|
7442
7597
|
return json({
|
|
7443
7598
|
version: 1,
|
|
7444
7599
|
mode: "shadow",
|
|
7445
7600
|
writable: true,
|
|
7446
7601
|
authoritativeForChapterOperation: false,
|
|
7602
|
+
// The store pages every collection to its explicit 10,000-row operational
|
|
7603
|
+
// bound and rejects larger state instead of returning a partial projection.
|
|
7604
|
+
chapterLinkProjectionComplete: true,
|
|
7605
|
+
personProjectionComplete: true,
|
|
7606
|
+
aliasProjectionComplete: true,
|
|
7447
7607
|
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7448
|
-
|
|
7449
|
-
aliases,
|
|
7450
|
-
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7451
|
-
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7452
|
-
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7453
|
-
id: event.id,
|
|
7454
|
-
kind: event.kind,
|
|
7455
|
-
personId: event.personId ?? null,
|
|
7456
|
-
aliasId: event.aliasId ?? null,
|
|
7457
|
-
actorId: event.actorId,
|
|
7458
|
-
mutationId: event.mutationId,
|
|
7459
|
-
createdAt: event.createdAt
|
|
7460
|
-
}))
|
|
7608
|
+
...projection
|
|
7461
7609
|
});
|
|
7462
7610
|
};
|
|
7463
7611
|
|
|
@@ -7650,12 +7798,12 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
7650
7798
|
const prior = latest.get(key);
|
|
7651
7799
|
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7652
7800
|
}
|
|
7653
|
-
const
|
|
7654
|
-
const customers =
|
|
7655
|
-
const subscriptions =
|
|
7656
|
-
const invoices =
|
|
7657
|
-
const refunds =
|
|
7658
|
-
for (const charge of
|
|
7801
|
+
const rows5 = [...latest.values()];
|
|
7802
|
+
const customers = rows5.filter((row) => row.objectKind === "customer");
|
|
7803
|
+
const subscriptions = rows5.filter((row) => row.objectKind === "subscription");
|
|
7804
|
+
const invoices = rows5.filter((row) => row.objectKind === "invoice");
|
|
7805
|
+
const refunds = rows5.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
7806
|
+
for (const charge of rows5.filter((row) => row.eventType === "charge.refunded")) {
|
|
7659
7807
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7660
7808
|
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7661
7809
|
}
|
|
@@ -7684,7 +7832,7 @@ var materializedNamespaces = [
|
|
|
7684
7832
|
"networkCommercialInvoices",
|
|
7685
7833
|
"networkCommercialRefunds"
|
|
7686
7834
|
];
|
|
7687
|
-
async function
|
|
7835
|
+
async function rows4(db, ns) {
|
|
7688
7836
|
const all = [];
|
|
7689
7837
|
const pageSize = 500;
|
|
7690
7838
|
for (let offset = 0; ; offset += pageSize) {
|
|
@@ -7695,9 +7843,9 @@ async function rows5(db, ns) {
|
|
|
7695
7843
|
}
|
|
7696
7844
|
}
|
|
7697
7845
|
async function loadNetworkCommercialProjection(db, sources) {
|
|
7698
|
-
const stored = await
|
|
7846
|
+
const stored = await rows4(db, "networkCommercialEvents");
|
|
7699
7847
|
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7700
|
-
const persisted = await
|
|
7848
|
+
const persisted = await rows4(db, "networkCommercialExceptions");
|
|
7701
7849
|
for (const row of persisted) {
|
|
7702
7850
|
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7703
7851
|
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
@@ -7735,8 +7883,8 @@ function projectionOps(projection, now) {
|
|
|
7735
7883
|
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7736
7884
|
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7737
7885
|
const [existing, ...materialized] = await Promise.all([
|
|
7738
|
-
|
|
7739
|
-
...materializedNamespaces.map((ns) =>
|
|
7886
|
+
rows4(db, "networkCommercialExceptions"),
|
|
7887
|
+
...materializedNamespaces.map((ns) => rows4(db, ns))
|
|
7740
7888
|
]);
|
|
7741
7889
|
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7742
7890
|
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
@@ -7845,11 +7993,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
|
7845
7993
|
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7846
7994
|
throw new Error("Stripe returned an invalid commercial list response");
|
|
7847
7995
|
}
|
|
7848
|
-
const
|
|
7849
|
-
if (
|
|
7850
|
-
objects.push(...
|
|
7996
|
+
const rows5 = page.data.map(asRaw).filter((row) => row !== null);
|
|
7997
|
+
if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
7998
|
+
objects.push(...rows5);
|
|
7851
7999
|
if (!page.has_more) return { objects, complete: true };
|
|
7852
|
-
const cursor =
|
|
8000
|
+
const cursor = rows5.at(-1)?.id;
|
|
7853
8001
|
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7854
8002
|
startingAfter = cursor;
|
|
7855
8003
|
}
|
|
@@ -8101,14 +8249,14 @@ var PAGE2 = 500;
|
|
|
8101
8249
|
var MAX_SCAN = 5e3;
|
|
8102
8250
|
var MAX_RECORDS2 = 500;
|
|
8103
8251
|
async function localCommercialRows(db) {
|
|
8104
|
-
const
|
|
8252
|
+
const rows5 = [];
|
|
8105
8253
|
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
8106
8254
|
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
8107
8255
|
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
8108
|
-
|
|
8109
|
-
if (page.length < PAGE2) return { rows:
|
|
8256
|
+
rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
|
|
8257
|
+
if (page.length < PAGE2) return { rows: rows5, truncated: false };
|
|
8110
8258
|
}
|
|
8111
|
-
return { rows:
|
|
8259
|
+
return { rows: rows5, truncated: true };
|
|
8112
8260
|
}
|
|
8113
8261
|
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
8114
8262
|
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
@@ -8667,17 +8815,17 @@ async function builderLibraryDigest(document) {
|
|
|
8667
8815
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8668
8816
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8669
8817
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
8670
|
-
const
|
|
8671
|
-
return Array.isArray(
|
|
8818
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
8819
|
+
return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
|
|
8672
8820
|
}
|
|
8673
8821
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
8674
|
-
const
|
|
8675
|
-
return control2(Array.isArray(
|
|
8822
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
8823
|
+
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8676
8824
|
}
|
|
8677
8825
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
8678
8826
|
const id = `builder-library-local:${environment}`;
|
|
8679
|
-
const
|
|
8680
|
-
return local2(Array.isArray(
|
|
8827
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
8828
|
+
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
8681
8829
|
}
|
|
8682
8830
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
8683
8831
|
const { document, digest: digest2 } = envelope;
|