@odla-ai/chapter 0.47.4 → 0.47.6
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 +574 -173
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +574 -173
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -375,8 +375,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
375
375
|
function byOrder(a, b) {
|
|
376
376
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
377
377
|
}
|
|
378
|
-
function resolveTiers(group,
|
|
379
|
-
const stored =
|
|
378
|
+
function resolveTiers(group, rows5 = []) {
|
|
379
|
+
const stored = rows5.filter((tier) => tier.active);
|
|
380
380
|
if (stored.length) return [...stored].sort(byOrder);
|
|
381
381
|
return [{
|
|
382
382
|
id: "standard",
|
|
@@ -422,15 +422,15 @@ function rowToTier(row, groupId) {
|
|
|
422
422
|
}
|
|
423
423
|
async function loadTiers(db, group) {
|
|
424
424
|
const groupId = String(group.id ?? "");
|
|
425
|
-
let
|
|
425
|
+
let rows5;
|
|
426
426
|
try {
|
|
427
|
-
|
|
427
|
+
rows5 = (await db.query({
|
|
428
428
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
429
429
|
})).tiers;
|
|
430
430
|
} catch {
|
|
431
|
-
|
|
431
|
+
rows5 = void 0;
|
|
432
432
|
}
|
|
433
|
-
const list = Array.isArray(
|
|
433
|
+
const list = Array.isArray(rows5) ? rows5.map((row) => rowToTier(row, groupId)) : [];
|
|
434
434
|
return resolveTiers(group, list);
|
|
435
435
|
}
|
|
436
436
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -1484,27 +1484,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1484
1484
|
if (db.aggregate) {
|
|
1485
1485
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1486
1486
|
}
|
|
1487
|
-
const
|
|
1488
|
-
return Array.isArray(
|
|
1487
|
+
const rows5 = (await db.query({ signupControls: {} })).signupControls;
|
|
1488
|
+
return Array.isArray(rows5) && rows5.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1489
1489
|
}
|
|
1490
1490
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1491
1491
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1492
|
-
const
|
|
1492
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1493
1493
|
where: { targetId, environment },
|
|
1494
1494
|
order: { revision: "desc" },
|
|
1495
1495
|
limit: 1
|
|
1496
1496
|
} } })).signupControls;
|
|
1497
|
-
const latest = Array.isArray(
|
|
1497
|
+
const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1498
1498
|
return asStored(latest);
|
|
1499
1499
|
}
|
|
1500
1500
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1501
1501
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1502
|
-
const
|
|
1502
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1503
1503
|
where: { targetId, environment, active: true },
|
|
1504
1504
|
order: { revision: "desc" },
|
|
1505
1505
|
limit: 1
|
|
1506
1506
|
} } })).signupControls;
|
|
1507
|
-
const active = Array.isArray(
|
|
1507
|
+
const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1508
1508
|
return asStored(active);
|
|
1509
1509
|
}
|
|
1510
1510
|
function controlledApplication(base, document) {
|
|
@@ -2880,33 +2880,33 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2880
2880
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2881
2881
|
async function exists(db, ns, where) {
|
|
2882
2882
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2883
|
-
const
|
|
2884
|
-
return
|
|
2883
|
+
const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2884
|
+
return rows5.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2885
2885
|
}
|
|
2886
2886
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2887
2887
|
const where = { targetId, environment, active: true };
|
|
2888
2888
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2889
|
-
const
|
|
2889
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2890
2890
|
where,
|
|
2891
2891
|
order: { revision: "desc" },
|
|
2892
2892
|
limit: 1
|
|
2893
2893
|
} } })).memberContentControls;
|
|
2894
|
-
return control(Array.isArray(
|
|
2894
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2895
2895
|
}
|
|
2896
2896
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2897
2897
|
const where = { targetId, environment };
|
|
2898
2898
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2899
|
-
const
|
|
2899
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2900
2900
|
where,
|
|
2901
2901
|
order: { revision: "desc" },
|
|
2902
2902
|
limit: 1
|
|
2903
2903
|
} } })).memberContentControls;
|
|
2904
|
-
return control(Array.isArray(
|
|
2904
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2905
2905
|
}
|
|
2906
2906
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2907
2907
|
const id = `member-content-local:${environment}`;
|
|
2908
|
-
const
|
|
2909
|
-
return local(Array.isArray(
|
|
2908
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2909
|
+
return local(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
2910
2910
|
}
|
|
2911
2911
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2912
2912
|
const { document, digest: digest2 } = envelope;
|
|
@@ -2961,12 +2961,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2961
2961
|
}
|
|
2962
2962
|
async function memberTierId(db, chapterId, email) {
|
|
2963
2963
|
if (!email) return null;
|
|
2964
|
-
let
|
|
2964
|
+
let rows5 = (await db.query({ applications: { $: {
|
|
2965
2965
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2966
2966
|
limit: 20
|
|
2967
2967
|
} } })).applications;
|
|
2968
|
-
if (!
|
|
2969
|
-
const latest = Array.isArray(
|
|
2968
|
+
if (!rows5?.length) rows5 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2969
|
+
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;
|
|
2970
2970
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2971
2971
|
}
|
|
2972
2972
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -3692,8 +3692,8 @@ function makeCalendar(env) {
|
|
|
3692
3692
|
}
|
|
3693
3693
|
async function firstRow(db, ns, q) {
|
|
3694
3694
|
const res = await db.query({ [ns]: { $: q } });
|
|
3695
|
-
const
|
|
3696
|
-
return Array.isArray(
|
|
3695
|
+
const rows5 = res[ns];
|
|
3696
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
3697
3697
|
}
|
|
3698
3698
|
async function computeSlots(cal, cfg) {
|
|
3699
3699
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -4093,8 +4093,8 @@ function createStripeProvider(config) {
|
|
|
4093
4093
|
const sk = requireSecret(secretKey);
|
|
4094
4094
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4095
4095
|
if (!charges.ok) fail("charges list", charges);
|
|
4096
|
-
const
|
|
4097
|
-
const paid =
|
|
4096
|
+
const rows5 = charges.body.data ?? [];
|
|
4097
|
+
const paid = rows5.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4098
4098
|
const charge = paid[paid.length - 1];
|
|
4099
4099
|
if (!charge) {
|
|
4100
4100
|
const err = new Error("no paid charge to refund");
|
|
@@ -4202,8 +4202,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4202
4202
|
throw new Error("gift issuance conflict");
|
|
4203
4203
|
}
|
|
4204
4204
|
}
|
|
4205
|
-
function buyerGiftClaims(
|
|
4206
|
-
return
|
|
4205
|
+
function buyerGiftClaims(rows5, now) {
|
|
4206
|
+
return rows5.map((row) => {
|
|
4207
4207
|
const stored = String(row.status ?? "pending");
|
|
4208
4208
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4209
4209
|
return {
|
|
@@ -4495,8 +4495,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4495
4495
|
|
|
4496
4496
|
// src/stripe-webhook-partition.ts
|
|
4497
4497
|
var firstRow2 = async (db, ns, q) => {
|
|
4498
|
-
const
|
|
4499
|
-
return Array.isArray(
|
|
4498
|
+
const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4499
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4500
4500
|
};
|
|
4501
4501
|
var stripeEventDigest = async (rawBody) => {
|
|
4502
4502
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4566,8 +4566,8 @@ async function replayedStripeReceipt(db, eventId, digest2, event) {
|
|
|
4566
4566
|
// src/worker-routes-payments.ts
|
|
4567
4567
|
async function firstRow3(db, ns, q) {
|
|
4568
4568
|
const res = await db.query({ [ns]: { $: q } });
|
|
4569
|
-
const
|
|
4570
|
-
return Array.isArray(
|
|
4569
|
+
const rows5 = res[ns];
|
|
4570
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4571
4571
|
}
|
|
4572
4572
|
async function findApplication(db, event) {
|
|
4573
4573
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -5033,8 +5033,8 @@ async function upcomingEvents(env) {
|
|
|
5033
5033
|
const res = await cal.availability.upcoming();
|
|
5034
5034
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5035
5035
|
}
|
|
5036
|
-
function toReconcile(
|
|
5037
|
-
return
|
|
5036
|
+
function toReconcile(rows5) {
|
|
5037
|
+
return rows5.map((m) => ({
|
|
5038
5038
|
id: String(m.id),
|
|
5039
5039
|
applicationId: String(m.applicationId),
|
|
5040
5040
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5056,12 +5056,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5056
5056
|
const query = await db.query({
|
|
5057
5057
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5058
5058
|
});
|
|
5059
|
-
let
|
|
5060
|
-
if (Number.isFinite(from))
|
|
5061
|
-
if (Number.isFinite(to))
|
|
5059
|
+
let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5060
|
+
if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5061
|
+
if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5062
5062
|
let decisions = [];
|
|
5063
5063
|
try {
|
|
5064
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5064
|
+
decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
|
|
5065
5065
|
} catch {
|
|
5066
5066
|
}
|
|
5067
5067
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5088,7 +5088,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5088
5088
|
};
|
|
5089
5089
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5090
5090
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5091
|
-
const meetings =
|
|
5091
|
+
const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5092
5092
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5093
5093
|
};
|
|
5094
5094
|
|
|
@@ -5164,10 +5164,10 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5164
5164
|
});
|
|
5165
5165
|
}
|
|
5166
5166
|
}
|
|
5167
|
-
const
|
|
5167
|
+
const rows5 = [...people.values()].sort(
|
|
5168
5168
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5169
5169
|
);
|
|
5170
|
-
return json({ people:
|
|
5170
|
+
return json({ people: rows5 });
|
|
5171
5171
|
};
|
|
5172
5172
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5173
5173
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
@@ -6894,6 +6894,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6894
6894
|
// src/network-person-registry.ts
|
|
6895
6895
|
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6896
6896
|
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6897
|
+
var FIXTURE_MARKER = /fixture|(?:^|[\s._:-])(dummy|sample|test(?:case)?)(?:$|[\s._:-])/i;
|
|
6897
6898
|
function normalizeNetworkIdentity(kind, value) {
|
|
6898
6899
|
const trimmed = value.trim();
|
|
6899
6900
|
if (kind === "email") return trimmed.toLowerCase();
|
|
@@ -6920,6 +6921,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
6920
6921
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6921
6922
|
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6922
6923
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6924
|
+
if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
|
|
6925
|
+
errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
|
|
6926
|
+
}
|
|
6923
6927
|
const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
|
|
6924
6928
|
const consentStates = ["unknown", "granted", "denied"];
|
|
6925
6929
|
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
@@ -7033,7 +7037,7 @@ function canonicalNetworkPersonId(personId, persons) {
|
|
|
7033
7037
|
}
|
|
7034
7038
|
}
|
|
7035
7039
|
|
|
7036
|
-
// src/network-person-store.ts
|
|
7040
|
+
// src/network-person-store-state.ts
|
|
7037
7041
|
var NetworkPersonRegistryError = class extends Error {
|
|
7038
7042
|
constructor(code, message) {
|
|
7039
7043
|
super(message);
|
|
@@ -7041,29 +7045,112 @@ var NetworkPersonRegistryError = class extends Error {
|
|
|
7041
7045
|
}
|
|
7042
7046
|
code;
|
|
7043
7047
|
};
|
|
7044
|
-
var
|
|
7045
|
-
async function loadNetworkPersonRegistry(db) {
|
|
7046
|
-
const result = await db.query({
|
|
7047
|
-
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
7048
|
-
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
7049
|
-
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
7050
|
-
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
7051
|
-
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
7052
|
-
});
|
|
7053
|
-
return {
|
|
7054
|
-
persons: rows4(result.networkPersons),
|
|
7055
|
-
claims: rows4(result.networkIdentityClaims),
|
|
7056
|
-
aliases: rows4(result.networkPersonAliases),
|
|
7057
|
-
links: rows4(result.networkChapterPersonLinks),
|
|
7058
|
-
events: rows4(result.networkPersonEvents)
|
|
7059
|
-
};
|
|
7060
|
-
}
|
|
7061
|
-
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7048
|
+
var networkPersonEventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7062
7049
|
t: "update",
|
|
7063
7050
|
ns: "networkPersonEvents",
|
|
7064
7051
|
id: `network_person_event:${mutationId}`,
|
|
7065
7052
|
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
7066
7053
|
});
|
|
7054
|
+
var asRows = (value) => Array.isArray(value) ? value : [];
|
|
7055
|
+
var MAX_ROWS = 1e4;
|
|
7056
|
+
async function boundedRows(db, ns, order2) {
|
|
7057
|
+
const collected = [];
|
|
7058
|
+
const pageSize = 500;
|
|
7059
|
+
for (let offset = 0; offset <= MAX_ROWS; offset += pageSize) {
|
|
7060
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset, ...order2 ? { order: order2 } : {} } } });
|
|
7061
|
+
const page = asRows(result[ns]);
|
|
7062
|
+
collected.push(...page);
|
|
7063
|
+
if (collected.length > MAX_ROWS) {
|
|
7064
|
+
throw new NetworkPersonRegistryError(
|
|
7065
|
+
"registry_too_large",
|
|
7066
|
+
`network Person registry ${ns} exceeds the ${MAX_ROWS.toLocaleString()}-row operational bound`
|
|
7067
|
+
);
|
|
7068
|
+
}
|
|
7069
|
+
if (page.length < pageSize) return collected;
|
|
7070
|
+
}
|
|
7071
|
+
return collected;
|
|
7072
|
+
}
|
|
7073
|
+
async function loadNetworkPersonRegistry(db) {
|
|
7074
|
+
const [persons, claims, aliases, links, events] = await Promise.all([
|
|
7075
|
+
boundedRows(db, "networkPersons", { updatedAt: "desc" }),
|
|
7076
|
+
boundedRows(db, "networkIdentityClaims"),
|
|
7077
|
+
boundedRows(db, "networkPersonAliases", { updatedAt: "desc" }),
|
|
7078
|
+
boundedRows(db, "networkChapterPersonLinks"),
|
|
7079
|
+
boundedRows(db, "networkPersonEvents", { createdAt: "desc" })
|
|
7080
|
+
]);
|
|
7081
|
+
return { persons, claims, aliases, links, events };
|
|
7082
|
+
}
|
|
7083
|
+
|
|
7084
|
+
// src/network-person-link-store.ts
|
|
7085
|
+
async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7086
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7087
|
+
const canonicalId = canonicalNetworkPersonId(input.personId, state.persons);
|
|
7088
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7089
|
+
if (!person || person.state !== "active" || canonicalId !== person.id) {
|
|
7090
|
+
throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
7091
|
+
}
|
|
7092
|
+
const sourceKey = await networkChapterPersonSourceKey(input.chapterId, input.chapterPersonId);
|
|
7093
|
+
const existingSource = state.links.find((link2) => link2.state === "shadow" && (link2.sourceKey === sourceKey || link2.chapterId === input.chapterId && link2.chapterPersonId === input.chapterPersonId));
|
|
7094
|
+
if (existingSource) {
|
|
7095
|
+
if (existingSource.personId !== person.id) {
|
|
7096
|
+
throw new NetworkPersonRegistryError("source_already_linked", "chapter source row is linked to another Person");
|
|
7097
|
+
}
|
|
7098
|
+
return { person, link: existingSource, replayed: true };
|
|
7099
|
+
}
|
|
7100
|
+
if (person.revision !== input.expectedPersonRevision) {
|
|
7101
|
+
throw new NetworkPersonRegistryError("stale_source_link", "Person changed; reload before linking this source row");
|
|
7102
|
+
}
|
|
7103
|
+
const expectedClaim = await networkIdentityClaim({ kind: "email", value: input.verifiedEmail, verifiedAt: 1 });
|
|
7104
|
+
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
7105
|
+
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
7106
|
+
}
|
|
7107
|
+
const id = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
7108
|
+
const previous = state.links.find((link2) => link2.id === id);
|
|
7109
|
+
const link = {
|
|
7110
|
+
id,
|
|
7111
|
+
personId: person.id,
|
|
7112
|
+
chapterId: input.chapterId,
|
|
7113
|
+
chapterPersonId: input.chapterPersonId,
|
|
7114
|
+
sourceKey,
|
|
7115
|
+
state: "shadow",
|
|
7116
|
+
createdAt: previous?.createdAt ?? now
|
|
7117
|
+
};
|
|
7118
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7119
|
+
await db.transact([
|
|
7120
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7121
|
+
{ t: "update", ns: "networkChapterPersonLinks", id, attrs: { ...link } },
|
|
7122
|
+
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
7123
|
+
], { mutationId, guards: [
|
|
7124
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7125
|
+
{ ns: "networkChapterPersonLinks", id, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
7126
|
+
] });
|
|
7127
|
+
return { person: nextPerson, link };
|
|
7128
|
+
}
|
|
7129
|
+
async function unlinkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7130
|
+
const reason = input.reason.trim();
|
|
7131
|
+
if (!reason || reason.length > 500) throw new NetworkPersonRegistryError("invalid_unlink", "a bounded unlink reason is required");
|
|
7132
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7133
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7134
|
+
const link = state.links.find((row) => row.id === input.linkId && row.personId === input.personId);
|
|
7135
|
+
if (!person || person.state !== "active" || person.revision !== input.expectedPersonRevision || !link || link.state !== "shadow") {
|
|
7136
|
+
throw new NetworkPersonRegistryError("stale_source_unlink", "Person or source link changed; reload before undoing the link");
|
|
7137
|
+
}
|
|
7138
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7139
|
+
const nextLink = { ...link, state: "revoked" };
|
|
7140
|
+
delete nextLink.sourceKey;
|
|
7141
|
+
await db.transact([
|
|
7142
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7143
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: link.id, attrs: { state: "revoked" } },
|
|
7144
|
+
{ t: "retract", ns: "networkChapterPersonLinks", id: link.id, attrs: ["sourceKey"] },
|
|
7145
|
+
networkPersonEventOp("chapter_source_unlinked", mutationId, actor, now, { chapterId: link.chapterId, reason }, { personId: person.id })
|
|
7146
|
+
], { mutationId, guards: [
|
|
7147
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7148
|
+
{ ns: "networkChapterPersonLinks", id: link.id, exists: true, equals: { state: "shadow", personId: person.id } }
|
|
7149
|
+
] });
|
|
7150
|
+
return { person: nextPerson, link: nextLink };
|
|
7151
|
+
}
|
|
7152
|
+
|
|
7153
|
+
// src/network-person-store.ts
|
|
7067
7154
|
var defaultPolicy = (now, consent) => ({
|
|
7068
7155
|
marketingSuppressed: consent.marketing !== "granted",
|
|
7069
7156
|
discretionarySuppressed: consent.discretionary !== "granted",
|
|
@@ -7073,7 +7160,8 @@ var isGuardConflict = (error) => error instanceof Error && error.code === "trans
|
|
|
7073
7160
|
async function linkOps(personId, links, now) {
|
|
7074
7161
|
return Promise.all(links.map(async (link) => {
|
|
7075
7162
|
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
7076
|
-
|
|
7163
|
+
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
7164
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
7077
7165
|
}));
|
|
7078
7166
|
}
|
|
7079
7167
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -7095,7 +7183,7 @@ async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId
|
|
|
7095
7183
|
};
|
|
7096
7184
|
await db.transact([
|
|
7097
7185
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
7098
|
-
|
|
7186
|
+
networkPersonEventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
7099
7187
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
7100
7188
|
return { status: "ambiguous", alias };
|
|
7101
7189
|
}
|
|
@@ -7131,7 +7219,7 @@ async function importNetworkPerson(db, input, mutationId, actor, now = Date.now(
|
|
|
7131
7219
|
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
7132
7220
|
...claimOps,
|
|
7133
7221
|
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
7134
|
-
|
|
7222
|
+
networkPersonEventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
7135
7223
|
];
|
|
7136
7224
|
try {
|
|
7137
7225
|
await db.transact(ops, { mutationId, guards: [
|
|
@@ -7153,12 +7241,21 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7153
7241
|
const state = await loadNetworkPersonRegistry(db);
|
|
7154
7242
|
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
7155
7243
|
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
7156
|
-
|
|
7244
|
+
const expectedStatus = input.decision === "reopen" ? "excluded" : "ambiguous";
|
|
7245
|
+
if (alias.revision !== input.expectedRevision || alias.status !== expectedStatus) throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
7246
|
+
if (input.decision === "reopen") {
|
|
7247
|
+
const next2 = { ...alias, status: "ambiguous", revision: alias.revision + 1, updatedAt: now };
|
|
7248
|
+
await db.transact([
|
|
7249
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7250
|
+
networkPersonEventOp("alias_reopened", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7251
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "excluded" } }] });
|
|
7252
|
+
return next2;
|
|
7253
|
+
}
|
|
7157
7254
|
if (input.decision === "exclude") {
|
|
7158
7255
|
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
7159
7256
|
await db.transact([
|
|
7160
7257
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7161
|
-
|
|
7258
|
+
networkPersonEventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7162
7259
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
7163
7260
|
return next2;
|
|
7164
7261
|
}
|
|
@@ -7174,7 +7271,7 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7174
7271
|
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7175
7272
|
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7176
7273
|
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7177
|
-
|
|
7274
|
+
networkPersonEventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7178
7275
|
], { mutationId, guards: [
|
|
7179
7276
|
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7180
7277
|
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
@@ -7204,7 +7301,7 @@ async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now(
|
|
|
7204
7301
|
await db.transact([
|
|
7205
7302
|
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7206
7303
|
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7207
|
-
|
|
7304
|
+
networkPersonEventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7208
7305
|
], { mutationId, guards: [
|
|
7209
7306
|
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7210
7307
|
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
@@ -7220,7 +7317,7 @@ async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now
|
|
|
7220
7317
|
await db.transact([
|
|
7221
7318
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7222
7319
|
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7223
|
-
|
|
7320
|
+
networkPersonEventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7224
7321
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7225
7322
|
return next;
|
|
7226
7323
|
}
|
|
@@ -7234,50 +7331,257 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
7234
7331
|
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7235
7332
|
await db.transact([
|
|
7236
7333
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7237
|
-
|
|
7334
|
+
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7238
7335
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7239
7336
|
return next;
|
|
7240
7337
|
}
|
|
7241
7338
|
|
|
7242
|
-
// src/network-person-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7339
|
+
// src/network-person-bulk-preview.ts
|
|
7340
|
+
var NETWORK_PERSON_BULK_IMPORT_MAX_ROWS = 5e3;
|
|
7341
|
+
var NETWORK_PERSON_BULK_IMPORT_VERSION = 1;
|
|
7342
|
+
var networkPersonBulkEmptyCounts = () => ({
|
|
7343
|
+
new: 0,
|
|
7344
|
+
replay: 0,
|
|
7345
|
+
exactMatchReview: 0,
|
|
7346
|
+
invalid: 0,
|
|
7347
|
+
rejected: 0,
|
|
7348
|
+
failed: 0
|
|
7349
|
+
});
|
|
7350
|
+
var incrementNetworkPersonBulkCount = (counts, status) => {
|
|
7351
|
+
if (status === "exact_match_review") counts.exactMatchReview += 1;
|
|
7352
|
+
else counts[status] += 1;
|
|
7353
|
+
};
|
|
7354
|
+
function stableValue(value) {
|
|
7355
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
7356
|
+
if (value && typeof value === "object") {
|
|
7357
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
|
|
7358
|
+
}
|
|
7359
|
+
return value;
|
|
7360
|
+
}
|
|
7361
|
+
async function networkPersonBulkSha256(value) {
|
|
7362
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
7363
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
7364
|
+
}
|
|
7365
|
+
function assertNetworkPersonBulkRows(rows5) {
|
|
7366
|
+
if (!Array.isArray(rows5) || rows5.length < 1) {
|
|
7367
|
+
throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
|
|
7247
7368
|
}
|
|
7369
|
+
if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
|
|
7370
|
+
throw new NetworkPersonRegistryError(
|
|
7371
|
+
"bulk_rows_exceeded",
|
|
7372
|
+
`bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
|
|
7373
|
+
);
|
|
7374
|
+
}
|
|
7375
|
+
return rows5;
|
|
7376
|
+
}
|
|
7377
|
+
async function networkPersonBulkBatchDigest(rows5) {
|
|
7378
|
+
const bounded = assertNetworkPersonBulkRows(rows5);
|
|
7379
|
+
return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
|
|
7380
|
+
}
|
|
7381
|
+
async function networkPersonRegistryStateDigest(state) {
|
|
7382
|
+
const snapshot = {
|
|
7383
|
+
persons: state.persons.map(({ id, revision, state: personState, mergedIntoId }) => ({ id, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7384
|
+
claims: state.claims.map(({ id, personId }) => ({ id, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7385
|
+
aliases: state.aliases.map(({ id, revision, status, personId }) => ({ id, revision, status, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7386
|
+
links: state.links.map(({ id, personId, state: linkState }) => ({ id, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
7387
|
+
};
|
|
7388
|
+
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
7389
|
+
}
|
|
7390
|
+
function rejectedValidation(errors) {
|
|
7391
|
+
return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
|
|
7392
|
+
}
|
|
7393
|
+
async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
7394
|
+
const aliasIds = new Set(state.aliases.map((alias) => alias.id));
|
|
7395
|
+
const batchAliases = /* @__PURE__ */ new Set();
|
|
7396
|
+
const claimOwners = /* @__PURE__ */ new Map();
|
|
7397
|
+
for (const claim of state.claims) {
|
|
7398
|
+
const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
|
|
7399
|
+
owners.add(claim.personId);
|
|
7400
|
+
claimOwners.set(claim.id, owners);
|
|
7401
|
+
}
|
|
7402
|
+
const results = [];
|
|
7403
|
+
const values = [];
|
|
7404
|
+
for (let index2 = 0; index2 < rows5.length; index2 += 1) {
|
|
7405
|
+
const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
|
|
7406
|
+
if (!validation.ok || !validation.value) {
|
|
7407
|
+
const errors = validation.errors.slice(0, 8);
|
|
7408
|
+
results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
|
|
7409
|
+
values.push(null);
|
|
7410
|
+
continue;
|
|
7411
|
+
}
|
|
7412
|
+
const value = validation.value;
|
|
7413
|
+
const aliasId = await networkSourceKey(value.sourceSystem, value.sourceRecordId);
|
|
7414
|
+
if (batchAliases.has(aliasId)) {
|
|
7415
|
+
results.push({ rowNumber: index2 + 2, status: "rejected", errors: ["duplicate source record in this batch"] });
|
|
7416
|
+
values.push(null);
|
|
7417
|
+
continue;
|
|
7418
|
+
}
|
|
7419
|
+
batchAliases.add(aliasId);
|
|
7420
|
+
if (aliasIds.has(aliasId)) {
|
|
7421
|
+
results.push({ rowNumber: index2 + 2, status: "replay" });
|
|
7422
|
+
values.push(value);
|
|
7423
|
+
continue;
|
|
7424
|
+
}
|
|
7425
|
+
const claims = await Promise.all(value.identities.map(networkIdentityClaim));
|
|
7426
|
+
const candidates = new Set(claims.flatMap((claim) => [...claimOwners.get(claim.id) ?? []]));
|
|
7427
|
+
results.push(candidates.size ? { rowNumber: index2 + 2, status: "exact_match_review", candidateCount: candidates.size } : { rowNumber: index2 + 2, status: "new" });
|
|
7428
|
+
const plannedOwner = `bulk_row_${index2 + 2}`;
|
|
7429
|
+
for (const claim of claims) {
|
|
7430
|
+
const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
|
|
7431
|
+
owners.add(plannedOwner);
|
|
7432
|
+
claimOwners.set(claim.id, owners);
|
|
7433
|
+
}
|
|
7434
|
+
values.push(value);
|
|
7435
|
+
}
|
|
7436
|
+
return { rows: results, values };
|
|
7437
|
+
}
|
|
7438
|
+
async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
|
|
7439
|
+
const bounded = assertNetworkPersonBulkRows(rows5);
|
|
7248
7440
|
const state = await loadNetworkPersonRegistry(db);
|
|
7249
|
-
const
|
|
7250
|
-
|
|
7251
|
-
const
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7441
|
+
const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
|
|
7442
|
+
const counts = networkPersonBulkEmptyCounts();
|
|
7443
|
+
for (const row of classified.rows) incrementNetworkPersonBulkCount(counts, row.status);
|
|
7444
|
+
return {
|
|
7445
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7446
|
+
batchDigest: await networkPersonBulkBatchDigest(bounded),
|
|
7447
|
+
stateDigest: await networkPersonRegistryStateDigest(state),
|
|
7448
|
+
rowCount: bounded.length,
|
|
7449
|
+
counts,
|
|
7450
|
+
rows: classified.rows
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
|
|
7454
|
+
// src/network-person-bulk-import.ts
|
|
7455
|
+
async function derivedMutationId(batchMutationId, part) {
|
|
7456
|
+
return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
|
|
7457
|
+
}
|
|
7458
|
+
function matchingEvent(events, mutationId, kind) {
|
|
7459
|
+
return events.find((event) => event.mutationId === mutationId && event.kind === kind);
|
|
7460
|
+
}
|
|
7461
|
+
function eventString(event, key) {
|
|
7462
|
+
return typeof event.detail[key] === "string" ? String(event.detail[key]) : "";
|
|
7463
|
+
}
|
|
7464
|
+
function eventCounts(event) {
|
|
7465
|
+
const value = event.detail.counts;
|
|
7466
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
|
|
7467
|
+
}
|
|
7468
|
+
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
7469
|
+
if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
|
|
7470
|
+
const rows5 = assertNetworkPersonBulkRows(input.rows);
|
|
7471
|
+
const batchDigest = await networkPersonBulkBatchDigest(rows5);
|
|
7472
|
+
if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
|
|
7473
|
+
throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
|
|
7474
|
+
}
|
|
7475
|
+
if (!/^[a-f0-9]{64}$/.test(input.expectedStateDigest)) {
|
|
7476
|
+
throw new NetworkPersonRegistryError("bulk_state_digest_required", "a reviewed registry state digest is required");
|
|
7477
|
+
}
|
|
7478
|
+
const startedMutationId = await derivedMutationId(batchMutationId, "started");
|
|
7479
|
+
const completedMutationId = await derivedMutationId(batchMutationId, "completed");
|
|
7480
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
7481
|
+
const completed = matchingEvent(state.events, completedMutationId, "bulk_import_completed");
|
|
7482
|
+
if (completed) {
|
|
7483
|
+
if (eventString(completed, "batchDigest") !== batchDigest) {
|
|
7484
|
+
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id was already used for different rows");
|
|
7485
|
+
}
|
|
7486
|
+
return {
|
|
7487
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7488
|
+
batchDigest,
|
|
7489
|
+
stateDigest: eventString(completed, "stateDigest"),
|
|
7490
|
+
rowCount: Number(completed.detail.rowCount) || rows5.length,
|
|
7491
|
+
counts: eventCounts(completed),
|
|
7492
|
+
rows: [],
|
|
7493
|
+
replayed: true
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
7497
|
+
if (!started) {
|
|
7498
|
+
const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
|
|
7499
|
+
if (preview.stateDigest !== input.expectedStateDigest) {
|
|
7500
|
+
throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
|
|
7501
|
+
}
|
|
7502
|
+
try {
|
|
7503
|
+
await db.transact([
|
|
7504
|
+
networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
|
|
7505
|
+
batchDigest,
|
|
7506
|
+
stateDigest: input.expectedStateDigest,
|
|
7507
|
+
rowCount: rows5.length,
|
|
7508
|
+
previewCounts: preview.counts
|
|
7509
|
+
})
|
|
7510
|
+
], {
|
|
7511
|
+
mutationId: startedMutationId,
|
|
7512
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${startedMutationId}`, exists: false }]
|
|
7513
|
+
});
|
|
7514
|
+
} catch (error) {
|
|
7515
|
+
if (!(error instanceof Error) || error.code !== "transact_guard_failed") throw error;
|
|
7516
|
+
}
|
|
7517
|
+
state = await loadNetworkPersonRegistry(db);
|
|
7518
|
+
started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
7519
|
+
}
|
|
7520
|
+
if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
|
|
7521
|
+
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
|
|
7522
|
+
}
|
|
7523
|
+
const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
|
|
7524
|
+
const results = [];
|
|
7525
|
+
const counts = networkPersonBulkEmptyCounts();
|
|
7526
|
+
for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
|
|
7527
|
+
const row = classified.rows[index2];
|
|
7528
|
+
const value = classified.values[index2];
|
|
7529
|
+
if (!value) {
|
|
7530
|
+
results.push(row);
|
|
7531
|
+
incrementNetworkPersonBulkCount(counts, row.status);
|
|
7532
|
+
continue;
|
|
7533
|
+
}
|
|
7534
|
+
try {
|
|
7535
|
+
const result = await importNetworkPerson(
|
|
7536
|
+
db,
|
|
7537
|
+
value,
|
|
7538
|
+
await derivedMutationId(batchMutationId, `row_${index2 + 2}`),
|
|
7539
|
+
actor,
|
|
7540
|
+
now + index2 + 1
|
|
7541
|
+
);
|
|
7542
|
+
const status = "replayed" in result && result.replayed ? "replay" : result.status === "ambiguous" ? "exact_match_review" : "new";
|
|
7543
|
+
const applied = {
|
|
7544
|
+
rowNumber: index2 + 2,
|
|
7545
|
+
status,
|
|
7546
|
+
...result.status === "ambiguous" ? { candidateCount: result.alias.candidatePersonIds?.length ?? 0 } : {}
|
|
7547
|
+
};
|
|
7548
|
+
results.push(applied);
|
|
7549
|
+
incrementNetworkPersonBulkCount(counts, status);
|
|
7550
|
+
} catch (error) {
|
|
7551
|
+
const failed = {
|
|
7552
|
+
rowNumber: index2 + 2,
|
|
7553
|
+
status: "failed",
|
|
7554
|
+
errors: [error instanceof Error ? error.message : "row failed"]
|
|
7555
|
+
};
|
|
7556
|
+
results.push(failed);
|
|
7557
|
+
incrementNetworkPersonBulkCount(counts, failed.status);
|
|
7264
7558
|
}
|
|
7559
|
+
}
|
|
7560
|
+
if (counts.failed === 0) {
|
|
7561
|
+
await db.transact([
|
|
7562
|
+
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
|
|
7563
|
+
batchDigest,
|
|
7564
|
+
stateDigest: input.expectedStateDigest,
|
|
7565
|
+
rowCount: rows5.length,
|
|
7566
|
+
counts
|
|
7567
|
+
})
|
|
7568
|
+
], { mutationId: completedMutationId });
|
|
7569
|
+
}
|
|
7570
|
+
return {
|
|
7571
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7572
|
+
batchDigest,
|
|
7573
|
+
stateDigest: input.expectedStateDigest,
|
|
7574
|
+
rowCount: rows5.length,
|
|
7575
|
+
counts,
|
|
7576
|
+
rows: results
|
|
7265
7577
|
};
|
|
7266
|
-
await db.transact([event], {
|
|
7267
|
-
mutationId,
|
|
7268
|
-
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7269
|
-
});
|
|
7270
|
-
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7271
7578
|
}
|
|
7272
7579
|
|
|
7273
|
-
// src/
|
|
7274
|
-
var PATH2 = "/api/admin/network-person-registry";
|
|
7275
|
-
var MAX_BODY2 = 32e3;
|
|
7276
|
-
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7580
|
+
// src/network-person-route-projection.ts
|
|
7277
7581
|
function redactedName(displayName) {
|
|
7278
7582
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7279
7583
|
}
|
|
7280
|
-
var
|
|
7584
|
+
var redactedNetworkPersonLink = async (link) => ({
|
|
7281
7585
|
id: link.id,
|
|
7282
7586
|
personId: link.personId,
|
|
7283
7587
|
chapterId: link.chapterId,
|
|
@@ -7285,13 +7589,13 @@ var redactedLink = async (link) => ({
|
|
|
7285
7589
|
state: link.state,
|
|
7286
7590
|
createdAt: link.createdAt
|
|
7287
7591
|
});
|
|
7288
|
-
var
|
|
7592
|
+
var safeNetworkContactPolicy = (policy) => ({
|
|
7289
7593
|
marketingSuppressed: policy.marketingSuppressed,
|
|
7290
7594
|
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7291
7595
|
hasReason: Boolean(policy.reason),
|
|
7292
7596
|
updatedAt: policy.updatedAt
|
|
7293
7597
|
});
|
|
7294
|
-
var
|
|
7598
|
+
var safeNetworkPersonAlias = async (alias) => ({
|
|
7295
7599
|
id: alias.id,
|
|
7296
7600
|
sourceSystem: alias.sourceSystem,
|
|
7297
7601
|
displayNameHint: redactedName(alias.displayName),
|
|
@@ -7306,17 +7610,91 @@ var safeAlias = (alias) => ({
|
|
|
7306
7610
|
},
|
|
7307
7611
|
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7308
7612
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7613
|
+
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
7614
|
+
chapterId: link.chapterId,
|
|
7615
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId)
|
|
7616
|
+
}))),
|
|
7309
7617
|
revision: alias.revision,
|
|
7310
7618
|
updatedAt: alias.updatedAt
|
|
7311
7619
|
});
|
|
7312
|
-
var
|
|
7620
|
+
var safeNetworkPerson = (person, claims = []) => ({
|
|
7313
7621
|
id: person.id,
|
|
7622
|
+
displayName: person.displayName,
|
|
7314
7623
|
displayNameHint: redactedName(person.displayName),
|
|
7624
|
+
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)),
|
|
7315
7625
|
state: person.state,
|
|
7316
7626
|
mergedIntoId: person.mergedIntoId ?? null,
|
|
7317
7627
|
revision: person.revision,
|
|
7318
|
-
contactPolicy:
|
|
7628
|
+
contactPolicy: safeNetworkContactPolicy(person.contactPolicy)
|
|
7319
7629
|
});
|
|
7630
|
+
async function projectNetworkPersonRegistryState(state) {
|
|
7631
|
+
const persons = await Promise.all(state.persons.map(async (person) => {
|
|
7632
|
+
const claims = state.claims.filter((claim) => claim.personId === person.id);
|
|
7633
|
+
const links = state.links.filter((link) => link.personId === person.id && link.state === "shadow");
|
|
7634
|
+
const safeLinks = await Promise.all(links.map(redactedNetworkPersonLink));
|
|
7635
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7636
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7637
|
+
return {
|
|
7638
|
+
...safeNetworkPerson(person, claims),
|
|
7639
|
+
canonicalPersonId: canonicalId,
|
|
7640
|
+
identityClaims: claims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7641
|
+
chapterLinks: safeLinks,
|
|
7642
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
7643
|
+
};
|
|
7644
|
+
}));
|
|
7645
|
+
const aliases = await Promise.all(state.aliases.map(safeNetworkPersonAlias));
|
|
7646
|
+
return {
|
|
7647
|
+
persons,
|
|
7648
|
+
aliases,
|
|
7649
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7650
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7651
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7652
|
+
id: event.id,
|
|
7653
|
+
kind: event.kind,
|
|
7654
|
+
personId: event.personId ?? null,
|
|
7655
|
+
aliasId: event.aliasId ?? null,
|
|
7656
|
+
actorId: event.actorId,
|
|
7657
|
+
mutationId: event.mutationId,
|
|
7658
|
+
createdAt: event.createdAt
|
|
7659
|
+
}))
|
|
7660
|
+
};
|
|
7661
|
+
}
|
|
7662
|
+
|
|
7663
|
+
// src/network-person-reveal.ts
|
|
7664
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7665
|
+
const reason = input.reason.trim();
|
|
7666
|
+
if (!reason || reason.length > 500) {
|
|
7667
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7668
|
+
}
|
|
7669
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7670
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7671
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7672
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7673
|
+
const event = {
|
|
7674
|
+
t: "update",
|
|
7675
|
+
ns: "networkPersonEvents",
|
|
7676
|
+
id: `network_person_event:${mutationId}`,
|
|
7677
|
+
attrs: {
|
|
7678
|
+
id: `network_person_event:${mutationId}`,
|
|
7679
|
+
kind: "pii_revealed",
|
|
7680
|
+
actorId: actor.id,
|
|
7681
|
+
...ids,
|
|
7682
|
+
mutationId,
|
|
7683
|
+
detail: { targetKind: input.kind, reason },
|
|
7684
|
+
createdAt: now
|
|
7685
|
+
}
|
|
7686
|
+
};
|
|
7687
|
+
await db.transact([event], {
|
|
7688
|
+
mutationId,
|
|
7689
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7690
|
+
});
|
|
7691
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7692
|
+
}
|
|
7693
|
+
|
|
7694
|
+
// src/worker-routes-network-person-registry.ts
|
|
7695
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7696
|
+
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
7697
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7320
7698
|
var positiveRevision = (value) => {
|
|
7321
7699
|
const parsed = Number(value);
|
|
7322
7700
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
@@ -7330,7 +7708,7 @@ async function gate10(req, env, ctx) {
|
|
|
7330
7708
|
}
|
|
7331
7709
|
function registryError(error) {
|
|
7332
7710
|
if (error instanceof NetworkPersonRegistryError) {
|
|
7333
|
-
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7711
|
+
const status = error.code.includes("stale") || error.code === "identity_conflict" || error.code === "bulk_state_changed" || error.code === "bulk_digest_changed" || error.code === "bulk_mutation_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7334
7712
|
return json({ error: error.message, code: error.code }, status);
|
|
7335
7713
|
}
|
|
7336
7714
|
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
@@ -7365,6 +7743,23 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7365
7743
|
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7366
7744
|
const actionActor = { id: got.user.userId };
|
|
7367
7745
|
try {
|
|
7746
|
+
if (command === "preview-bulk-import") {
|
|
7747
|
+
const result = await previewNetworkPersonBulkImport(
|
|
7748
|
+
db,
|
|
7749
|
+
input.rows,
|
|
7750
|
+
ctx.chapter.networkPersonRegistry.chapterIds
|
|
7751
|
+
);
|
|
7752
|
+
return json({ result });
|
|
7753
|
+
}
|
|
7754
|
+
if (command === "apply-bulk-import") {
|
|
7755
|
+
const result = await applyNetworkPersonBulkImport(db, {
|
|
7756
|
+
rows: input.rows,
|
|
7757
|
+
expectedBatchDigest: typeof input.expectedBatchDigest === "string" ? input.expectedBatchDigest : "",
|
|
7758
|
+
expectedStateDigest: typeof input.expectedStateDigest === "string" ? input.expectedStateDigest : "",
|
|
7759
|
+
reviewed: input.reviewed === true
|
|
7760
|
+
}, mutationId, actionActor, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7761
|
+
return json({ result }, result.replayed ? 200 : 201);
|
|
7762
|
+
}
|
|
7368
7763
|
if (command === "import") {
|
|
7369
7764
|
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7370
7765
|
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|
|
@@ -7372,14 +7767,14 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7372
7767
|
return json({
|
|
7373
7768
|
result: {
|
|
7374
7769
|
status: result.status,
|
|
7375
|
-
alias:
|
|
7376
|
-
..."person" in result && result.person ? { person:
|
|
7770
|
+
alias: await safeNetworkPersonAlias(result.alias),
|
|
7771
|
+
..."person" in result && result.person ? { person: safeNetworkPerson(result.person, result.alias.identityClaims) } : {},
|
|
7377
7772
|
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7378
7773
|
}
|
|
7379
7774
|
}, 201);
|
|
7380
7775
|
}
|
|
7381
7776
|
if (command === "review") {
|
|
7382
|
-
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or
|
|
7777
|
+
if (input.decision !== "link" && input.decision !== "exclude" && input.decision !== "reopen") return json({ error: "review decision must be link, exclude, or reopen" }, 400);
|
|
7383
7778
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7384
7779
|
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7385
7780
|
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
@@ -7389,7 +7784,42 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7389
7784
|
decision: input.decision,
|
|
7390
7785
|
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7391
7786
|
}, mutationId, actionActor);
|
|
7392
|
-
return json({ result:
|
|
7787
|
+
return json({ result: await safeNetworkPersonAlias(result) });
|
|
7788
|
+
}
|
|
7789
|
+
if (command === "link-source") {
|
|
7790
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7791
|
+
const chapterId = typeof input.chapterId === "string" ? input.chapterId : "";
|
|
7792
|
+
const chapterPersonId = typeof input.chapterPersonId === "string" ? input.chapterPersonId.trim() : "";
|
|
7793
|
+
const verifiedEmail = typeof input.verifiedEmail === "string" ? input.verifiedEmail.trim() : "";
|
|
7794
|
+
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);
|
|
7795
|
+
const result = await linkNetworkChapterPerson(db, {
|
|
7796
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7797
|
+
expectedPersonRevision,
|
|
7798
|
+
chapterId,
|
|
7799
|
+
chapterPersonId,
|
|
7800
|
+
verifiedEmail
|
|
7801
|
+
}, mutationId, actionActor);
|
|
7802
|
+
return json({
|
|
7803
|
+
result: {
|
|
7804
|
+
person: safeNetworkPerson(result.person),
|
|
7805
|
+
link: await redactedNetworkPersonLink(result.link),
|
|
7806
|
+
...result.replayed ? { replayed: true } : {}
|
|
7807
|
+
}
|
|
7808
|
+
});
|
|
7809
|
+
}
|
|
7810
|
+
if (command === "unlink-source") {
|
|
7811
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7812
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7813
|
+
if (!expectedPersonRevision || !reason || reason.length > 500) {
|
|
7814
|
+
return json({ error: "a Person revision and bounded unlink reason are required" }, 400);
|
|
7815
|
+
}
|
|
7816
|
+
const result = await unlinkNetworkChapterPerson(db, {
|
|
7817
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7818
|
+
linkId: typeof input.linkId === "string" ? input.linkId : "",
|
|
7819
|
+
expectedPersonRevision,
|
|
7820
|
+
reason
|
|
7821
|
+
}, mutationId, actionActor);
|
|
7822
|
+
return json({ result: { person: safeNetworkPerson(result.person), linkState: result.link.state } });
|
|
7393
7823
|
}
|
|
7394
7824
|
if (command === "merge") {
|
|
7395
7825
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7401,7 +7831,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7401
7831
|
expectedRevision,
|
|
7402
7832
|
expectedTargetRevision
|
|
7403
7833
|
}, mutationId, actionActor);
|
|
7404
|
-
return json({ result: { source:
|
|
7834
|
+
return json({ result: { source: safeNetworkPerson(result.source), target: safeNetworkPerson(result.target) } });
|
|
7405
7835
|
}
|
|
7406
7836
|
if (command === "unmerge") {
|
|
7407
7837
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7410,7 +7840,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7410
7840
|
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7411
7841
|
expectedRevision
|
|
7412
7842
|
}, mutationId, actionActor);
|
|
7413
|
-
return json({ result:
|
|
7843
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7414
7844
|
}
|
|
7415
7845
|
if (command === "suppression") {
|
|
7416
7846
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7426,7 +7856,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7426
7856
|
discretionarySuppressed: input.discretionarySuppressed,
|
|
7427
7857
|
...reason ? { reason } : {}
|
|
7428
7858
|
}, mutationId, actionActor);
|
|
7429
|
-
return json({ result:
|
|
7859
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7430
7860
|
}
|
|
7431
7861
|
if (command === "reveal") {
|
|
7432
7862
|
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
@@ -7445,48 +7875,19 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7445
7875
|
}
|
|
7446
7876
|
}
|
|
7447
7877
|
const state = await loadNetworkPersonRegistry(db);
|
|
7448
|
-
const
|
|
7449
|
-
const links = state.links.filter((link) => link.personId === person.id);
|
|
7450
|
-
const safeLinks = await Promise.all(links.map(redactedLink));
|
|
7451
|
-
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7452
|
-
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7453
|
-
return {
|
|
7454
|
-
id: person.id,
|
|
7455
|
-
displayNameHint: redactedName(person.displayName),
|
|
7456
|
-
state: person.state,
|
|
7457
|
-
canonicalPersonId: canonicalId,
|
|
7458
|
-
mergedIntoId: person.mergedIntoId ?? null,
|
|
7459
|
-
revision: person.revision,
|
|
7460
|
-
contactPolicy: safePolicy(person.contactPolicy),
|
|
7461
|
-
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7462
|
-
chapterLinks: safeLinks,
|
|
7463
|
-
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7464
|
-
};
|
|
7465
|
-
}));
|
|
7466
|
-
const aliases = state.aliases.map(safeAlias);
|
|
7878
|
+
const projection = await projectNetworkPersonRegistryState(state);
|
|
7467
7879
|
return json({
|
|
7468
7880
|
version: 1,
|
|
7469
7881
|
mode: "shadow",
|
|
7470
7882
|
writable: true,
|
|
7471
7883
|
authoritativeForChapterOperation: false,
|
|
7472
|
-
//
|
|
7473
|
-
//
|
|
7474
|
-
|
|
7475
|
-
|
|
7884
|
+
// The store pages every collection to its explicit 10,000-row operational
|
|
7885
|
+
// bound and rejects larger state instead of returning a partial projection.
|
|
7886
|
+
chapterLinkProjectionComplete: true,
|
|
7887
|
+
personProjectionComplete: true,
|
|
7888
|
+
aliasProjectionComplete: true,
|
|
7476
7889
|
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7477
|
-
|
|
7478
|
-
aliases,
|
|
7479
|
-
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7480
|
-
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7481
|
-
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7482
|
-
id: event.id,
|
|
7483
|
-
kind: event.kind,
|
|
7484
|
-
personId: event.personId ?? null,
|
|
7485
|
-
aliasId: event.aliasId ?? null,
|
|
7486
|
-
actorId: event.actorId,
|
|
7487
|
-
mutationId: event.mutationId,
|
|
7488
|
-
createdAt: event.createdAt
|
|
7489
|
-
}))
|
|
7890
|
+
...projection
|
|
7490
7891
|
});
|
|
7491
7892
|
};
|
|
7492
7893
|
|
|
@@ -7679,12 +8080,12 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
7679
8080
|
const prior = latest.get(key);
|
|
7680
8081
|
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7681
8082
|
}
|
|
7682
|
-
const
|
|
7683
|
-
const customers =
|
|
7684
|
-
const subscriptions =
|
|
7685
|
-
const invoices =
|
|
7686
|
-
const refunds =
|
|
7687
|
-
for (const charge of
|
|
8083
|
+
const rows5 = [...latest.values()];
|
|
8084
|
+
const customers = rows5.filter((row) => row.objectKind === "customer");
|
|
8085
|
+
const subscriptions = rows5.filter((row) => row.objectKind === "subscription");
|
|
8086
|
+
const invoices = rows5.filter((row) => row.objectKind === "invoice");
|
|
8087
|
+
const refunds = rows5.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
8088
|
+
for (const charge of rows5.filter((row) => row.eventType === "charge.refunded")) {
|
|
7688
8089
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7689
8090
|
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7690
8091
|
}
|
|
@@ -7713,7 +8114,7 @@ var materializedNamespaces = [
|
|
|
7713
8114
|
"networkCommercialInvoices",
|
|
7714
8115
|
"networkCommercialRefunds"
|
|
7715
8116
|
];
|
|
7716
|
-
async function
|
|
8117
|
+
async function rows4(db, ns) {
|
|
7717
8118
|
const all = [];
|
|
7718
8119
|
const pageSize = 500;
|
|
7719
8120
|
for (let offset = 0; ; offset += pageSize) {
|
|
@@ -7724,9 +8125,9 @@ async function rows5(db, ns) {
|
|
|
7724
8125
|
}
|
|
7725
8126
|
}
|
|
7726
8127
|
async function loadNetworkCommercialProjection(db, sources) {
|
|
7727
|
-
const stored = await
|
|
8128
|
+
const stored = await rows4(db, "networkCommercialEvents");
|
|
7728
8129
|
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7729
|
-
const persisted = await
|
|
8130
|
+
const persisted = await rows4(db, "networkCommercialExceptions");
|
|
7730
8131
|
for (const row of persisted) {
|
|
7731
8132
|
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7732
8133
|
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
@@ -7764,8 +8165,8 @@ function projectionOps(projection, now) {
|
|
|
7764
8165
|
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7765
8166
|
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7766
8167
|
const [existing, ...materialized] = await Promise.all([
|
|
7767
|
-
|
|
7768
|
-
...materializedNamespaces.map((ns) =>
|
|
8168
|
+
rows4(db, "networkCommercialExceptions"),
|
|
8169
|
+
...materializedNamespaces.map((ns) => rows4(db, ns))
|
|
7769
8170
|
]);
|
|
7770
8171
|
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7771
8172
|
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
@@ -7874,11 +8275,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
|
7874
8275
|
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7875
8276
|
throw new Error("Stripe returned an invalid commercial list response");
|
|
7876
8277
|
}
|
|
7877
|
-
const
|
|
7878
|
-
if (
|
|
7879
|
-
objects.push(...
|
|
8278
|
+
const rows5 = page.data.map(asRaw).filter((row) => row !== null);
|
|
8279
|
+
if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
8280
|
+
objects.push(...rows5);
|
|
7880
8281
|
if (!page.has_more) return { objects, complete: true };
|
|
7881
|
-
const cursor =
|
|
8282
|
+
const cursor = rows5.at(-1)?.id;
|
|
7882
8283
|
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7883
8284
|
startingAfter = cursor;
|
|
7884
8285
|
}
|
|
@@ -8130,14 +8531,14 @@ var PAGE2 = 500;
|
|
|
8130
8531
|
var MAX_SCAN = 5e3;
|
|
8131
8532
|
var MAX_RECORDS2 = 500;
|
|
8132
8533
|
async function localCommercialRows(db) {
|
|
8133
|
-
const
|
|
8534
|
+
const rows5 = [];
|
|
8134
8535
|
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
8135
8536
|
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
8136
8537
|
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
8137
|
-
|
|
8138
|
-
if (page.length < PAGE2) return { rows:
|
|
8538
|
+
rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
|
|
8539
|
+
if (page.length < PAGE2) return { rows: rows5, truncated: false };
|
|
8139
8540
|
}
|
|
8140
|
-
return { rows:
|
|
8541
|
+
return { rows: rows5, truncated: true };
|
|
8141
8542
|
}
|
|
8142
8543
|
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
8143
8544
|
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
@@ -8696,17 +9097,17 @@ async function builderLibraryDigest(document) {
|
|
|
8696
9097
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8697
9098
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8698
9099
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
8699
|
-
const
|
|
8700
|
-
return Array.isArray(
|
|
9100
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
9101
|
+
return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
|
|
8701
9102
|
}
|
|
8702
9103
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
8703
|
-
const
|
|
8704
|
-
return control2(Array.isArray(
|
|
9104
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
9105
|
+
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8705
9106
|
}
|
|
8706
9107
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
8707
9108
|
const id = `builder-library-local:${environment}`;
|
|
8708
|
-
const
|
|
8709
|
-
return local2(Array.isArray(
|
|
9109
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
9110
|
+
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
8710
9111
|
}
|
|
8711
9112
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
8712
9113
|
const { document, digest: digest2 } = envelope;
|