@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.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;
|
|
@@ -6874,6 +6874,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6874
6874
|
// src/network-person-registry.ts
|
|
6875
6875
|
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6876
6876
|
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6877
|
+
var FIXTURE_MARKER = /fixture|(?:^|[\s._:-])(dummy|sample|test(?:case)?)(?:$|[\s._:-])/i;
|
|
6877
6878
|
function normalizeNetworkIdentity(kind, value) {
|
|
6878
6879
|
const trimmed = value.trim();
|
|
6879
6880
|
if (kind === "email") return trimmed.toLowerCase();
|
|
@@ -6900,6 +6901,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
6900
6901
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6901
6902
|
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6902
6903
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6904
|
+
if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
|
|
6905
|
+
errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
|
|
6906
|
+
}
|
|
6903
6907
|
const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
|
|
6904
6908
|
const consentStates = ["unknown", "granted", "denied"];
|
|
6905
6909
|
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
@@ -7013,7 +7017,7 @@ function canonicalNetworkPersonId(personId, persons) {
|
|
|
7013
7017
|
}
|
|
7014
7018
|
}
|
|
7015
7019
|
|
|
7016
|
-
// src/network-person-store.ts
|
|
7020
|
+
// src/network-person-store-state.ts
|
|
7017
7021
|
var NetworkPersonRegistryError = class extends Error {
|
|
7018
7022
|
constructor(code, message) {
|
|
7019
7023
|
super(message);
|
|
@@ -7021,29 +7025,112 @@ var NetworkPersonRegistryError = class extends Error {
|
|
|
7021
7025
|
}
|
|
7022
7026
|
code;
|
|
7023
7027
|
};
|
|
7024
|
-
var
|
|
7025
|
-
async function loadNetworkPersonRegistry(db) {
|
|
7026
|
-
const result = await db.query({
|
|
7027
|
-
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
7028
|
-
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
7029
|
-
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
7030
|
-
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
7031
|
-
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
7032
|
-
});
|
|
7033
|
-
return {
|
|
7034
|
-
persons: rows4(result.networkPersons),
|
|
7035
|
-
claims: rows4(result.networkIdentityClaims),
|
|
7036
|
-
aliases: rows4(result.networkPersonAliases),
|
|
7037
|
-
links: rows4(result.networkChapterPersonLinks),
|
|
7038
|
-
events: rows4(result.networkPersonEvents)
|
|
7039
|
-
};
|
|
7040
|
-
}
|
|
7041
|
-
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7028
|
+
var networkPersonEventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7042
7029
|
t: "update",
|
|
7043
7030
|
ns: "networkPersonEvents",
|
|
7044
7031
|
id: `network_person_event:${mutationId}`,
|
|
7045
7032
|
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
7046
7033
|
});
|
|
7034
|
+
var asRows = (value) => Array.isArray(value) ? value : [];
|
|
7035
|
+
var MAX_ROWS = 1e4;
|
|
7036
|
+
async function boundedRows(db, ns, order2) {
|
|
7037
|
+
const collected = [];
|
|
7038
|
+
const pageSize = 500;
|
|
7039
|
+
for (let offset = 0; offset <= MAX_ROWS; offset += pageSize) {
|
|
7040
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset, ...order2 ? { order: order2 } : {} } } });
|
|
7041
|
+
const page = asRows(result[ns]);
|
|
7042
|
+
collected.push(...page);
|
|
7043
|
+
if (collected.length > MAX_ROWS) {
|
|
7044
|
+
throw new NetworkPersonRegistryError(
|
|
7045
|
+
"registry_too_large",
|
|
7046
|
+
`network Person registry ${ns} exceeds the ${MAX_ROWS.toLocaleString()}-row operational bound`
|
|
7047
|
+
);
|
|
7048
|
+
}
|
|
7049
|
+
if (page.length < pageSize) return collected;
|
|
7050
|
+
}
|
|
7051
|
+
return collected;
|
|
7052
|
+
}
|
|
7053
|
+
async function loadNetworkPersonRegistry(db) {
|
|
7054
|
+
const [persons, claims, aliases, links, events] = await Promise.all([
|
|
7055
|
+
boundedRows(db, "networkPersons", { updatedAt: "desc" }),
|
|
7056
|
+
boundedRows(db, "networkIdentityClaims"),
|
|
7057
|
+
boundedRows(db, "networkPersonAliases", { updatedAt: "desc" }),
|
|
7058
|
+
boundedRows(db, "networkChapterPersonLinks"),
|
|
7059
|
+
boundedRows(db, "networkPersonEvents", { createdAt: "desc" })
|
|
7060
|
+
]);
|
|
7061
|
+
return { persons, claims, aliases, links, events };
|
|
7062
|
+
}
|
|
7063
|
+
|
|
7064
|
+
// src/network-person-link-store.ts
|
|
7065
|
+
async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7066
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7067
|
+
const canonicalId = canonicalNetworkPersonId(input.personId, state.persons);
|
|
7068
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7069
|
+
if (!person || person.state !== "active" || canonicalId !== person.id) {
|
|
7070
|
+
throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
7071
|
+
}
|
|
7072
|
+
const sourceKey = await networkChapterPersonSourceKey(input.chapterId, input.chapterPersonId);
|
|
7073
|
+
const existingSource = state.links.find((link2) => link2.state === "shadow" && (link2.sourceKey === sourceKey || link2.chapterId === input.chapterId && link2.chapterPersonId === input.chapterPersonId));
|
|
7074
|
+
if (existingSource) {
|
|
7075
|
+
if (existingSource.personId !== person.id) {
|
|
7076
|
+
throw new NetworkPersonRegistryError("source_already_linked", "chapter source row is linked to another Person");
|
|
7077
|
+
}
|
|
7078
|
+
return { person, link: existingSource, replayed: true };
|
|
7079
|
+
}
|
|
7080
|
+
if (person.revision !== input.expectedPersonRevision) {
|
|
7081
|
+
throw new NetworkPersonRegistryError("stale_source_link", "Person changed; reload before linking this source row");
|
|
7082
|
+
}
|
|
7083
|
+
const expectedClaim = await networkIdentityClaim({ kind: "email", value: input.verifiedEmail, verifiedAt: 1 });
|
|
7084
|
+
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
7085
|
+
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
7086
|
+
}
|
|
7087
|
+
const id = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
7088
|
+
const previous = state.links.find((link2) => link2.id === id);
|
|
7089
|
+
const link = {
|
|
7090
|
+
id,
|
|
7091
|
+
personId: person.id,
|
|
7092
|
+
chapterId: input.chapterId,
|
|
7093
|
+
chapterPersonId: input.chapterPersonId,
|
|
7094
|
+
sourceKey,
|
|
7095
|
+
state: "shadow",
|
|
7096
|
+
createdAt: previous?.createdAt ?? now
|
|
7097
|
+
};
|
|
7098
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7099
|
+
await db.transact([
|
|
7100
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7101
|
+
{ t: "update", ns: "networkChapterPersonLinks", id, attrs: { ...link } },
|
|
7102
|
+
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
7103
|
+
], { mutationId, guards: [
|
|
7104
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7105
|
+
{ ns: "networkChapterPersonLinks", id, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
7106
|
+
] });
|
|
7107
|
+
return { person: nextPerson, link };
|
|
7108
|
+
}
|
|
7109
|
+
async function unlinkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7110
|
+
const reason = input.reason.trim();
|
|
7111
|
+
if (!reason || reason.length > 500) throw new NetworkPersonRegistryError("invalid_unlink", "a bounded unlink reason is required");
|
|
7112
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7113
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7114
|
+
const link = state.links.find((row) => row.id === input.linkId && row.personId === input.personId);
|
|
7115
|
+
if (!person || person.state !== "active" || person.revision !== input.expectedPersonRevision || !link || link.state !== "shadow") {
|
|
7116
|
+
throw new NetworkPersonRegistryError("stale_source_unlink", "Person or source link changed; reload before undoing the link");
|
|
7117
|
+
}
|
|
7118
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7119
|
+
const nextLink = { ...link, state: "revoked" };
|
|
7120
|
+
delete nextLink.sourceKey;
|
|
7121
|
+
await db.transact([
|
|
7122
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7123
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: link.id, attrs: { state: "revoked" } },
|
|
7124
|
+
{ t: "retract", ns: "networkChapterPersonLinks", id: link.id, attrs: ["sourceKey"] },
|
|
7125
|
+
networkPersonEventOp("chapter_source_unlinked", mutationId, actor, now, { chapterId: link.chapterId, reason }, { personId: person.id })
|
|
7126
|
+
], { mutationId, guards: [
|
|
7127
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7128
|
+
{ ns: "networkChapterPersonLinks", id: link.id, exists: true, equals: { state: "shadow", personId: person.id } }
|
|
7129
|
+
] });
|
|
7130
|
+
return { person: nextPerson, link: nextLink };
|
|
7131
|
+
}
|
|
7132
|
+
|
|
7133
|
+
// src/network-person-store.ts
|
|
7047
7134
|
var defaultPolicy = (now, consent) => ({
|
|
7048
7135
|
marketingSuppressed: consent.marketing !== "granted",
|
|
7049
7136
|
discretionarySuppressed: consent.discretionary !== "granted",
|
|
@@ -7053,7 +7140,8 @@ var isGuardConflict = (error) => error instanceof Error && error.code === "trans
|
|
|
7053
7140
|
async function linkOps(personId, links, now) {
|
|
7054
7141
|
return Promise.all(links.map(async (link) => {
|
|
7055
7142
|
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
7056
|
-
|
|
7143
|
+
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
7144
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
7057
7145
|
}));
|
|
7058
7146
|
}
|
|
7059
7147
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -7075,7 +7163,7 @@ async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId
|
|
|
7075
7163
|
};
|
|
7076
7164
|
await db.transact([
|
|
7077
7165
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
7078
|
-
|
|
7166
|
+
networkPersonEventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
7079
7167
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
7080
7168
|
return { status: "ambiguous", alias };
|
|
7081
7169
|
}
|
|
@@ -7111,7 +7199,7 @@ async function importNetworkPerson(db, input, mutationId, actor, now = Date.now(
|
|
|
7111
7199
|
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
7112
7200
|
...claimOps,
|
|
7113
7201
|
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
7114
|
-
|
|
7202
|
+
networkPersonEventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
7115
7203
|
];
|
|
7116
7204
|
try {
|
|
7117
7205
|
await db.transact(ops, { mutationId, guards: [
|
|
@@ -7133,12 +7221,21 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7133
7221
|
const state = await loadNetworkPersonRegistry(db);
|
|
7134
7222
|
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
7135
7223
|
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
7136
|
-
|
|
7224
|
+
const expectedStatus = input.decision === "reopen" ? "excluded" : "ambiguous";
|
|
7225
|
+
if (alias.revision !== input.expectedRevision || alias.status !== expectedStatus) throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
7226
|
+
if (input.decision === "reopen") {
|
|
7227
|
+
const next2 = { ...alias, status: "ambiguous", revision: alias.revision + 1, updatedAt: now };
|
|
7228
|
+
await db.transact([
|
|
7229
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7230
|
+
networkPersonEventOp("alias_reopened", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7231
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "excluded" } }] });
|
|
7232
|
+
return next2;
|
|
7233
|
+
}
|
|
7137
7234
|
if (input.decision === "exclude") {
|
|
7138
7235
|
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
7139
7236
|
await db.transact([
|
|
7140
7237
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7141
|
-
|
|
7238
|
+
networkPersonEventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7142
7239
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
7143
7240
|
return next2;
|
|
7144
7241
|
}
|
|
@@ -7154,7 +7251,7 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7154
7251
|
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7155
7252
|
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7156
7253
|
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7157
|
-
|
|
7254
|
+
networkPersonEventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7158
7255
|
], { mutationId, guards: [
|
|
7159
7256
|
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7160
7257
|
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
@@ -7184,7 +7281,7 @@ async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now(
|
|
|
7184
7281
|
await db.transact([
|
|
7185
7282
|
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7186
7283
|
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7187
|
-
|
|
7284
|
+
networkPersonEventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7188
7285
|
], { mutationId, guards: [
|
|
7189
7286
|
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7190
7287
|
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
@@ -7200,7 +7297,7 @@ async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now
|
|
|
7200
7297
|
await db.transact([
|
|
7201
7298
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7202
7299
|
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7203
|
-
|
|
7300
|
+
networkPersonEventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7204
7301
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7205
7302
|
return next;
|
|
7206
7303
|
}
|
|
@@ -7214,50 +7311,257 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
7214
7311
|
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7215
7312
|
await db.transact([
|
|
7216
7313
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7217
|
-
|
|
7314
|
+
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7218
7315
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7219
7316
|
return next;
|
|
7220
7317
|
}
|
|
7221
7318
|
|
|
7222
|
-
// src/network-person-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7319
|
+
// src/network-person-bulk-preview.ts
|
|
7320
|
+
var NETWORK_PERSON_BULK_IMPORT_MAX_ROWS = 5e3;
|
|
7321
|
+
var NETWORK_PERSON_BULK_IMPORT_VERSION = 1;
|
|
7322
|
+
var networkPersonBulkEmptyCounts = () => ({
|
|
7323
|
+
new: 0,
|
|
7324
|
+
replay: 0,
|
|
7325
|
+
exactMatchReview: 0,
|
|
7326
|
+
invalid: 0,
|
|
7327
|
+
rejected: 0,
|
|
7328
|
+
failed: 0
|
|
7329
|
+
});
|
|
7330
|
+
var incrementNetworkPersonBulkCount = (counts, status) => {
|
|
7331
|
+
if (status === "exact_match_review") counts.exactMatchReview += 1;
|
|
7332
|
+
else counts[status] += 1;
|
|
7333
|
+
};
|
|
7334
|
+
function stableValue(value) {
|
|
7335
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
7336
|
+
if (value && typeof value === "object") {
|
|
7337
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
|
|
7338
|
+
}
|
|
7339
|
+
return value;
|
|
7340
|
+
}
|
|
7341
|
+
async function networkPersonBulkSha256(value) {
|
|
7342
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
7343
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
7344
|
+
}
|
|
7345
|
+
function assertNetworkPersonBulkRows(rows5) {
|
|
7346
|
+
if (!Array.isArray(rows5) || rows5.length < 1) {
|
|
7347
|
+
throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
|
|
7227
7348
|
}
|
|
7349
|
+
if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
|
|
7350
|
+
throw new NetworkPersonRegistryError(
|
|
7351
|
+
"bulk_rows_exceeded",
|
|
7352
|
+
`bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
|
|
7353
|
+
);
|
|
7354
|
+
}
|
|
7355
|
+
return rows5;
|
|
7356
|
+
}
|
|
7357
|
+
async function networkPersonBulkBatchDigest(rows5) {
|
|
7358
|
+
const bounded = assertNetworkPersonBulkRows(rows5);
|
|
7359
|
+
return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
|
|
7360
|
+
}
|
|
7361
|
+
async function networkPersonRegistryStateDigest(state) {
|
|
7362
|
+
const snapshot = {
|
|
7363
|
+
persons: state.persons.map(({ id, revision, state: personState, mergedIntoId }) => ({ id, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7364
|
+
claims: state.claims.map(({ id, personId }) => ({ id, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7365
|
+
aliases: state.aliases.map(({ id, revision, status, personId }) => ({ id, revision, status, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
7366
|
+
links: state.links.map(({ id, personId, state: linkState }) => ({ id, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
7367
|
+
};
|
|
7368
|
+
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
7369
|
+
}
|
|
7370
|
+
function rejectedValidation(errors) {
|
|
7371
|
+
return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
|
|
7372
|
+
}
|
|
7373
|
+
async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
|
|
7374
|
+
const aliasIds = new Set(state.aliases.map((alias) => alias.id));
|
|
7375
|
+
const batchAliases = /* @__PURE__ */ new Set();
|
|
7376
|
+
const claimOwners = /* @__PURE__ */ new Map();
|
|
7377
|
+
for (const claim of state.claims) {
|
|
7378
|
+
const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
|
|
7379
|
+
owners.add(claim.personId);
|
|
7380
|
+
claimOwners.set(claim.id, owners);
|
|
7381
|
+
}
|
|
7382
|
+
const results = [];
|
|
7383
|
+
const values = [];
|
|
7384
|
+
for (let index2 = 0; index2 < rows5.length; index2 += 1) {
|
|
7385
|
+
const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
|
|
7386
|
+
if (!validation.ok || !validation.value) {
|
|
7387
|
+
const errors = validation.errors.slice(0, 8);
|
|
7388
|
+
results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
|
|
7389
|
+
values.push(null);
|
|
7390
|
+
continue;
|
|
7391
|
+
}
|
|
7392
|
+
const value = validation.value;
|
|
7393
|
+
const aliasId = await networkSourceKey(value.sourceSystem, value.sourceRecordId);
|
|
7394
|
+
if (batchAliases.has(aliasId)) {
|
|
7395
|
+
results.push({ rowNumber: index2 + 2, status: "rejected", errors: ["duplicate source record in this batch"] });
|
|
7396
|
+
values.push(null);
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
batchAliases.add(aliasId);
|
|
7400
|
+
if (aliasIds.has(aliasId)) {
|
|
7401
|
+
results.push({ rowNumber: index2 + 2, status: "replay" });
|
|
7402
|
+
values.push(value);
|
|
7403
|
+
continue;
|
|
7404
|
+
}
|
|
7405
|
+
const claims = await Promise.all(value.identities.map(networkIdentityClaim));
|
|
7406
|
+
const candidates = new Set(claims.flatMap((claim) => [...claimOwners.get(claim.id) ?? []]));
|
|
7407
|
+
results.push(candidates.size ? { rowNumber: index2 + 2, status: "exact_match_review", candidateCount: candidates.size } : { rowNumber: index2 + 2, status: "new" });
|
|
7408
|
+
const plannedOwner = `bulk_row_${index2 + 2}`;
|
|
7409
|
+
for (const claim of claims) {
|
|
7410
|
+
const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
|
|
7411
|
+
owners.add(plannedOwner);
|
|
7412
|
+
claimOwners.set(claim.id, owners);
|
|
7413
|
+
}
|
|
7414
|
+
values.push(value);
|
|
7415
|
+
}
|
|
7416
|
+
return { rows: results, values };
|
|
7417
|
+
}
|
|
7418
|
+
async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
|
|
7419
|
+
const bounded = assertNetworkPersonBulkRows(rows5);
|
|
7228
7420
|
const state = await loadNetworkPersonRegistry(db);
|
|
7229
|
-
const
|
|
7230
|
-
|
|
7231
|
-
const
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7421
|
+
const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
|
|
7422
|
+
const counts = networkPersonBulkEmptyCounts();
|
|
7423
|
+
for (const row of classified.rows) incrementNetworkPersonBulkCount(counts, row.status);
|
|
7424
|
+
return {
|
|
7425
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7426
|
+
batchDigest: await networkPersonBulkBatchDigest(bounded),
|
|
7427
|
+
stateDigest: await networkPersonRegistryStateDigest(state),
|
|
7428
|
+
rowCount: bounded.length,
|
|
7429
|
+
counts,
|
|
7430
|
+
rows: classified.rows
|
|
7431
|
+
};
|
|
7432
|
+
}
|
|
7433
|
+
|
|
7434
|
+
// src/network-person-bulk-import.ts
|
|
7435
|
+
async function derivedMutationId(batchMutationId, part) {
|
|
7436
|
+
return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
|
|
7437
|
+
}
|
|
7438
|
+
function matchingEvent(events, mutationId, kind) {
|
|
7439
|
+
return events.find((event) => event.mutationId === mutationId && event.kind === kind);
|
|
7440
|
+
}
|
|
7441
|
+
function eventString(event, key) {
|
|
7442
|
+
return typeof event.detail[key] === "string" ? String(event.detail[key]) : "";
|
|
7443
|
+
}
|
|
7444
|
+
function eventCounts(event) {
|
|
7445
|
+
const value = event.detail.counts;
|
|
7446
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
|
|
7447
|
+
}
|
|
7448
|
+
async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
|
|
7449
|
+
if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
|
|
7450
|
+
const rows5 = assertNetworkPersonBulkRows(input.rows);
|
|
7451
|
+
const batchDigest = await networkPersonBulkBatchDigest(rows5);
|
|
7452
|
+
if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
|
|
7453
|
+
throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
|
|
7454
|
+
}
|
|
7455
|
+
if (!/^[a-f0-9]{64}$/.test(input.expectedStateDigest)) {
|
|
7456
|
+
throw new NetworkPersonRegistryError("bulk_state_digest_required", "a reviewed registry state digest is required");
|
|
7457
|
+
}
|
|
7458
|
+
const startedMutationId = await derivedMutationId(batchMutationId, "started");
|
|
7459
|
+
const completedMutationId = await derivedMutationId(batchMutationId, "completed");
|
|
7460
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
7461
|
+
const completed = matchingEvent(state.events, completedMutationId, "bulk_import_completed");
|
|
7462
|
+
if (completed) {
|
|
7463
|
+
if (eventString(completed, "batchDigest") !== batchDigest) {
|
|
7464
|
+
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id was already used for different rows");
|
|
7465
|
+
}
|
|
7466
|
+
return {
|
|
7467
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7468
|
+
batchDigest,
|
|
7469
|
+
stateDigest: eventString(completed, "stateDigest"),
|
|
7470
|
+
rowCount: Number(completed.detail.rowCount) || rows5.length,
|
|
7471
|
+
counts: eventCounts(completed),
|
|
7472
|
+
rows: [],
|
|
7473
|
+
replayed: true
|
|
7474
|
+
};
|
|
7475
|
+
}
|
|
7476
|
+
let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
7477
|
+
if (!started) {
|
|
7478
|
+
const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
|
|
7479
|
+
if (preview.stateDigest !== input.expectedStateDigest) {
|
|
7480
|
+
throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
|
|
7481
|
+
}
|
|
7482
|
+
try {
|
|
7483
|
+
await db.transact([
|
|
7484
|
+
networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
|
|
7485
|
+
batchDigest,
|
|
7486
|
+
stateDigest: input.expectedStateDigest,
|
|
7487
|
+
rowCount: rows5.length,
|
|
7488
|
+
previewCounts: preview.counts
|
|
7489
|
+
})
|
|
7490
|
+
], {
|
|
7491
|
+
mutationId: startedMutationId,
|
|
7492
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${startedMutationId}`, exists: false }]
|
|
7493
|
+
});
|
|
7494
|
+
} catch (error) {
|
|
7495
|
+
if (!(error instanceof Error) || error.code !== "transact_guard_failed") throw error;
|
|
7496
|
+
}
|
|
7497
|
+
state = await loadNetworkPersonRegistry(db);
|
|
7498
|
+
started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
|
|
7499
|
+
}
|
|
7500
|
+
if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
|
|
7501
|
+
throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
|
|
7502
|
+
}
|
|
7503
|
+
const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
|
|
7504
|
+
const results = [];
|
|
7505
|
+
const counts = networkPersonBulkEmptyCounts();
|
|
7506
|
+
for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
|
|
7507
|
+
const row = classified.rows[index2];
|
|
7508
|
+
const value = classified.values[index2];
|
|
7509
|
+
if (!value) {
|
|
7510
|
+
results.push(row);
|
|
7511
|
+
incrementNetworkPersonBulkCount(counts, row.status);
|
|
7512
|
+
continue;
|
|
7513
|
+
}
|
|
7514
|
+
try {
|
|
7515
|
+
const result = await importNetworkPerson(
|
|
7516
|
+
db,
|
|
7517
|
+
value,
|
|
7518
|
+
await derivedMutationId(batchMutationId, `row_${index2 + 2}`),
|
|
7519
|
+
actor,
|
|
7520
|
+
now + index2 + 1
|
|
7521
|
+
);
|
|
7522
|
+
const status = "replayed" in result && result.replayed ? "replay" : result.status === "ambiguous" ? "exact_match_review" : "new";
|
|
7523
|
+
const applied = {
|
|
7524
|
+
rowNumber: index2 + 2,
|
|
7525
|
+
status,
|
|
7526
|
+
...result.status === "ambiguous" ? { candidateCount: result.alias.candidatePersonIds?.length ?? 0 } : {}
|
|
7527
|
+
};
|
|
7528
|
+
results.push(applied);
|
|
7529
|
+
incrementNetworkPersonBulkCount(counts, status);
|
|
7530
|
+
} catch (error) {
|
|
7531
|
+
const failed = {
|
|
7532
|
+
rowNumber: index2 + 2,
|
|
7533
|
+
status: "failed",
|
|
7534
|
+
errors: [error instanceof Error ? error.message : "row failed"]
|
|
7535
|
+
};
|
|
7536
|
+
results.push(failed);
|
|
7537
|
+
incrementNetworkPersonBulkCount(counts, failed.status);
|
|
7244
7538
|
}
|
|
7539
|
+
}
|
|
7540
|
+
if (counts.failed === 0) {
|
|
7541
|
+
await db.transact([
|
|
7542
|
+
networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
|
|
7543
|
+
batchDigest,
|
|
7544
|
+
stateDigest: input.expectedStateDigest,
|
|
7545
|
+
rowCount: rows5.length,
|
|
7546
|
+
counts
|
|
7547
|
+
})
|
|
7548
|
+
], { mutationId: completedMutationId });
|
|
7549
|
+
}
|
|
7550
|
+
return {
|
|
7551
|
+
version: NETWORK_PERSON_BULK_IMPORT_VERSION,
|
|
7552
|
+
batchDigest,
|
|
7553
|
+
stateDigest: input.expectedStateDigest,
|
|
7554
|
+
rowCount: rows5.length,
|
|
7555
|
+
counts,
|
|
7556
|
+
rows: results
|
|
7245
7557
|
};
|
|
7246
|
-
await db.transact([event], {
|
|
7247
|
-
mutationId,
|
|
7248
|
-
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7249
|
-
});
|
|
7250
|
-
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7251
7558
|
}
|
|
7252
7559
|
|
|
7253
|
-
// src/
|
|
7254
|
-
var PATH2 = "/api/admin/network-person-registry";
|
|
7255
|
-
var MAX_BODY2 = 32e3;
|
|
7256
|
-
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7560
|
+
// src/network-person-route-projection.ts
|
|
7257
7561
|
function redactedName(displayName) {
|
|
7258
7562
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7259
7563
|
}
|
|
7260
|
-
var
|
|
7564
|
+
var redactedNetworkPersonLink = async (link) => ({
|
|
7261
7565
|
id: link.id,
|
|
7262
7566
|
personId: link.personId,
|
|
7263
7567
|
chapterId: link.chapterId,
|
|
@@ -7265,13 +7569,13 @@ var redactedLink = async (link) => ({
|
|
|
7265
7569
|
state: link.state,
|
|
7266
7570
|
createdAt: link.createdAt
|
|
7267
7571
|
});
|
|
7268
|
-
var
|
|
7572
|
+
var safeNetworkContactPolicy = (policy) => ({
|
|
7269
7573
|
marketingSuppressed: policy.marketingSuppressed,
|
|
7270
7574
|
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7271
7575
|
hasReason: Boolean(policy.reason),
|
|
7272
7576
|
updatedAt: policy.updatedAt
|
|
7273
7577
|
});
|
|
7274
|
-
var
|
|
7578
|
+
var safeNetworkPersonAlias = async (alias) => ({
|
|
7275
7579
|
id: alias.id,
|
|
7276
7580
|
sourceSystem: alias.sourceSystem,
|
|
7277
7581
|
displayNameHint: redactedName(alias.displayName),
|
|
@@ -7286,17 +7590,91 @@ var safeAlias = (alias) => ({
|
|
|
7286
7590
|
},
|
|
7287
7591
|
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7288
7592
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7593
|
+
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
7594
|
+
chapterId: link.chapterId,
|
|
7595
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId)
|
|
7596
|
+
}))),
|
|
7289
7597
|
revision: alias.revision,
|
|
7290
7598
|
updatedAt: alias.updatedAt
|
|
7291
7599
|
});
|
|
7292
|
-
var
|
|
7600
|
+
var safeNetworkPerson = (person, claims = []) => ({
|
|
7293
7601
|
id: person.id,
|
|
7602
|
+
displayName: person.displayName,
|
|
7294
7603
|
displayNameHint: redactedName(person.displayName),
|
|
7604
|
+
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)),
|
|
7295
7605
|
state: person.state,
|
|
7296
7606
|
mergedIntoId: person.mergedIntoId ?? null,
|
|
7297
7607
|
revision: person.revision,
|
|
7298
|
-
contactPolicy:
|
|
7608
|
+
contactPolicy: safeNetworkContactPolicy(person.contactPolicy)
|
|
7299
7609
|
});
|
|
7610
|
+
async function projectNetworkPersonRegistryState(state) {
|
|
7611
|
+
const persons = await Promise.all(state.persons.map(async (person) => {
|
|
7612
|
+
const claims = state.claims.filter((claim) => claim.personId === person.id);
|
|
7613
|
+
const links = state.links.filter((link) => link.personId === person.id && link.state === "shadow");
|
|
7614
|
+
const safeLinks = await Promise.all(links.map(redactedNetworkPersonLink));
|
|
7615
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7616
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7617
|
+
return {
|
|
7618
|
+
...safeNetworkPerson(person, claims),
|
|
7619
|
+
canonicalPersonId: canonicalId,
|
|
7620
|
+
identityClaims: claims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7621
|
+
chapterLinks: safeLinks,
|
|
7622
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
7623
|
+
};
|
|
7624
|
+
}));
|
|
7625
|
+
const aliases = await Promise.all(state.aliases.map(safeNetworkPersonAlias));
|
|
7626
|
+
return {
|
|
7627
|
+
persons,
|
|
7628
|
+
aliases,
|
|
7629
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7630
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7631
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7632
|
+
id: event.id,
|
|
7633
|
+
kind: event.kind,
|
|
7634
|
+
personId: event.personId ?? null,
|
|
7635
|
+
aliasId: event.aliasId ?? null,
|
|
7636
|
+
actorId: event.actorId,
|
|
7637
|
+
mutationId: event.mutationId,
|
|
7638
|
+
createdAt: event.createdAt
|
|
7639
|
+
}))
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
// src/network-person-reveal.ts
|
|
7644
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7645
|
+
const reason = input.reason.trim();
|
|
7646
|
+
if (!reason || reason.length > 500) {
|
|
7647
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7648
|
+
}
|
|
7649
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7650
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7651
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7652
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7653
|
+
const event = {
|
|
7654
|
+
t: "update",
|
|
7655
|
+
ns: "networkPersonEvents",
|
|
7656
|
+
id: `network_person_event:${mutationId}`,
|
|
7657
|
+
attrs: {
|
|
7658
|
+
id: `network_person_event:${mutationId}`,
|
|
7659
|
+
kind: "pii_revealed",
|
|
7660
|
+
actorId: actor.id,
|
|
7661
|
+
...ids,
|
|
7662
|
+
mutationId,
|
|
7663
|
+
detail: { targetKind: input.kind, reason },
|
|
7664
|
+
createdAt: now
|
|
7665
|
+
}
|
|
7666
|
+
};
|
|
7667
|
+
await db.transact([event], {
|
|
7668
|
+
mutationId,
|
|
7669
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7670
|
+
});
|
|
7671
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7672
|
+
}
|
|
7673
|
+
|
|
7674
|
+
// src/worker-routes-network-person-registry.ts
|
|
7675
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7676
|
+
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
7677
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7300
7678
|
var positiveRevision = (value) => {
|
|
7301
7679
|
const parsed = Number(value);
|
|
7302
7680
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
@@ -7310,7 +7688,7 @@ async function gate10(req, env, ctx) {
|
|
|
7310
7688
|
}
|
|
7311
7689
|
function registryError(error) {
|
|
7312
7690
|
if (error instanceof NetworkPersonRegistryError) {
|
|
7313
|
-
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7691
|
+
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;
|
|
7314
7692
|
return json({ error: error.message, code: error.code }, status);
|
|
7315
7693
|
}
|
|
7316
7694
|
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
@@ -7345,6 +7723,23 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7345
7723
|
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7346
7724
|
const actionActor = { id: got.user.userId };
|
|
7347
7725
|
try {
|
|
7726
|
+
if (command === "preview-bulk-import") {
|
|
7727
|
+
const result = await previewNetworkPersonBulkImport(
|
|
7728
|
+
db,
|
|
7729
|
+
input.rows,
|
|
7730
|
+
ctx.chapter.networkPersonRegistry.chapterIds
|
|
7731
|
+
);
|
|
7732
|
+
return json({ result });
|
|
7733
|
+
}
|
|
7734
|
+
if (command === "apply-bulk-import") {
|
|
7735
|
+
const result = await applyNetworkPersonBulkImport(db, {
|
|
7736
|
+
rows: input.rows,
|
|
7737
|
+
expectedBatchDigest: typeof input.expectedBatchDigest === "string" ? input.expectedBatchDigest : "",
|
|
7738
|
+
expectedStateDigest: typeof input.expectedStateDigest === "string" ? input.expectedStateDigest : "",
|
|
7739
|
+
reviewed: input.reviewed === true
|
|
7740
|
+
}, mutationId, actionActor, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7741
|
+
return json({ result }, result.replayed ? 200 : 201);
|
|
7742
|
+
}
|
|
7348
7743
|
if (command === "import") {
|
|
7349
7744
|
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7350
7745
|
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|
|
@@ -7352,14 +7747,14 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7352
7747
|
return json({
|
|
7353
7748
|
result: {
|
|
7354
7749
|
status: result.status,
|
|
7355
|
-
alias:
|
|
7356
|
-
..."person" in result && result.person ? { person:
|
|
7750
|
+
alias: await safeNetworkPersonAlias(result.alias),
|
|
7751
|
+
..."person" in result && result.person ? { person: safeNetworkPerson(result.person, result.alias.identityClaims) } : {},
|
|
7357
7752
|
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7358
7753
|
}
|
|
7359
7754
|
}, 201);
|
|
7360
7755
|
}
|
|
7361
7756
|
if (command === "review") {
|
|
7362
|
-
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or
|
|
7757
|
+
if (input.decision !== "link" && input.decision !== "exclude" && input.decision !== "reopen") return json({ error: "review decision must be link, exclude, or reopen" }, 400);
|
|
7363
7758
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7364
7759
|
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7365
7760
|
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
@@ -7369,7 +7764,42 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7369
7764
|
decision: input.decision,
|
|
7370
7765
|
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7371
7766
|
}, mutationId, actionActor);
|
|
7372
|
-
return json({ result:
|
|
7767
|
+
return json({ result: await safeNetworkPersonAlias(result) });
|
|
7768
|
+
}
|
|
7769
|
+
if (command === "link-source") {
|
|
7770
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7771
|
+
const chapterId = typeof input.chapterId === "string" ? input.chapterId : "";
|
|
7772
|
+
const chapterPersonId = typeof input.chapterPersonId === "string" ? input.chapterPersonId.trim() : "";
|
|
7773
|
+
const verifiedEmail = typeof input.verifiedEmail === "string" ? input.verifiedEmail.trim() : "";
|
|
7774
|
+
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);
|
|
7775
|
+
const result = await linkNetworkChapterPerson(db, {
|
|
7776
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7777
|
+
expectedPersonRevision,
|
|
7778
|
+
chapterId,
|
|
7779
|
+
chapterPersonId,
|
|
7780
|
+
verifiedEmail
|
|
7781
|
+
}, mutationId, actionActor);
|
|
7782
|
+
return json({
|
|
7783
|
+
result: {
|
|
7784
|
+
person: safeNetworkPerson(result.person),
|
|
7785
|
+
link: await redactedNetworkPersonLink(result.link),
|
|
7786
|
+
...result.replayed ? { replayed: true } : {}
|
|
7787
|
+
}
|
|
7788
|
+
});
|
|
7789
|
+
}
|
|
7790
|
+
if (command === "unlink-source") {
|
|
7791
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7792
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7793
|
+
if (!expectedPersonRevision || !reason || reason.length > 500) {
|
|
7794
|
+
return json({ error: "a Person revision and bounded unlink reason are required" }, 400);
|
|
7795
|
+
}
|
|
7796
|
+
const result = await unlinkNetworkChapterPerson(db, {
|
|
7797
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7798
|
+
linkId: typeof input.linkId === "string" ? input.linkId : "",
|
|
7799
|
+
expectedPersonRevision,
|
|
7800
|
+
reason
|
|
7801
|
+
}, mutationId, actionActor);
|
|
7802
|
+
return json({ result: { person: safeNetworkPerson(result.person), linkState: result.link.state } });
|
|
7373
7803
|
}
|
|
7374
7804
|
if (command === "merge") {
|
|
7375
7805
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7381,7 +7811,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7381
7811
|
expectedRevision,
|
|
7382
7812
|
expectedTargetRevision
|
|
7383
7813
|
}, mutationId, actionActor);
|
|
7384
|
-
return json({ result: { source:
|
|
7814
|
+
return json({ result: { source: safeNetworkPerson(result.source), target: safeNetworkPerson(result.target) } });
|
|
7385
7815
|
}
|
|
7386
7816
|
if (command === "unmerge") {
|
|
7387
7817
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7390,7 +7820,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7390
7820
|
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7391
7821
|
expectedRevision
|
|
7392
7822
|
}, mutationId, actionActor);
|
|
7393
|
-
return json({ result:
|
|
7823
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7394
7824
|
}
|
|
7395
7825
|
if (command === "suppression") {
|
|
7396
7826
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7406,7 +7836,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7406
7836
|
discretionarySuppressed: input.discretionarySuppressed,
|
|
7407
7837
|
...reason ? { reason } : {}
|
|
7408
7838
|
}, mutationId, actionActor);
|
|
7409
|
-
return json({ result:
|
|
7839
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7410
7840
|
}
|
|
7411
7841
|
if (command === "reveal") {
|
|
7412
7842
|
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
@@ -7425,48 +7855,19 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7425
7855
|
}
|
|
7426
7856
|
}
|
|
7427
7857
|
const state = await loadNetworkPersonRegistry(db);
|
|
7428
|
-
const
|
|
7429
|
-
const links = state.links.filter((link) => link.personId === person.id);
|
|
7430
|
-
const safeLinks = await Promise.all(links.map(redactedLink));
|
|
7431
|
-
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7432
|
-
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7433
|
-
return {
|
|
7434
|
-
id: person.id,
|
|
7435
|
-
displayNameHint: redactedName(person.displayName),
|
|
7436
|
-
state: person.state,
|
|
7437
|
-
canonicalPersonId: canonicalId,
|
|
7438
|
-
mergedIntoId: person.mergedIntoId ?? null,
|
|
7439
|
-
revision: person.revision,
|
|
7440
|
-
contactPolicy: safePolicy(person.contactPolicy),
|
|
7441
|
-
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7442
|
-
chapterLinks: safeLinks,
|
|
7443
|
-
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7444
|
-
};
|
|
7445
|
-
}));
|
|
7446
|
-
const aliases = state.aliases.map(safeAlias);
|
|
7858
|
+
const projection = await projectNetworkPersonRegistryState(state);
|
|
7447
7859
|
return json({
|
|
7448
7860
|
version: 1,
|
|
7449
7861
|
mode: "shadow",
|
|
7450
7862
|
writable: true,
|
|
7451
7863
|
authoritativeForChapterOperation: false,
|
|
7452
|
-
//
|
|
7453
|
-
//
|
|
7454
|
-
|
|
7455
|
-
|
|
7864
|
+
// The store pages every collection to its explicit 10,000-row operational
|
|
7865
|
+
// bound and rejects larger state instead of returning a partial projection.
|
|
7866
|
+
chapterLinkProjectionComplete: true,
|
|
7867
|
+
personProjectionComplete: true,
|
|
7868
|
+
aliasProjectionComplete: true,
|
|
7456
7869
|
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7457
|
-
|
|
7458
|
-
aliases,
|
|
7459
|
-
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7460
|
-
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7461
|
-
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7462
|
-
id: event.id,
|
|
7463
|
-
kind: event.kind,
|
|
7464
|
-
personId: event.personId ?? null,
|
|
7465
|
-
aliasId: event.aliasId ?? null,
|
|
7466
|
-
actorId: event.actorId,
|
|
7467
|
-
mutationId: event.mutationId,
|
|
7468
|
-
createdAt: event.createdAt
|
|
7469
|
-
}))
|
|
7870
|
+
...projection
|
|
7470
7871
|
});
|
|
7471
7872
|
};
|
|
7472
7873
|
|
|
@@ -7659,12 +8060,12 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
7659
8060
|
const prior = latest.get(key);
|
|
7660
8061
|
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7661
8062
|
}
|
|
7662
|
-
const
|
|
7663
|
-
const customers =
|
|
7664
|
-
const subscriptions =
|
|
7665
|
-
const invoices =
|
|
7666
|
-
const refunds =
|
|
7667
|
-
for (const charge of
|
|
8063
|
+
const rows5 = [...latest.values()];
|
|
8064
|
+
const customers = rows5.filter((row) => row.objectKind === "customer");
|
|
8065
|
+
const subscriptions = rows5.filter((row) => row.objectKind === "subscription");
|
|
8066
|
+
const invoices = rows5.filter((row) => row.objectKind === "invoice");
|
|
8067
|
+
const refunds = rows5.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
8068
|
+
for (const charge of rows5.filter((row) => row.eventType === "charge.refunded")) {
|
|
7668
8069
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7669
8070
|
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7670
8071
|
}
|
|
@@ -7693,7 +8094,7 @@ var materializedNamespaces = [
|
|
|
7693
8094
|
"networkCommercialInvoices",
|
|
7694
8095
|
"networkCommercialRefunds"
|
|
7695
8096
|
];
|
|
7696
|
-
async function
|
|
8097
|
+
async function rows4(db, ns) {
|
|
7697
8098
|
const all = [];
|
|
7698
8099
|
const pageSize = 500;
|
|
7699
8100
|
for (let offset = 0; ; offset += pageSize) {
|
|
@@ -7704,9 +8105,9 @@ async function rows5(db, ns) {
|
|
|
7704
8105
|
}
|
|
7705
8106
|
}
|
|
7706
8107
|
async function loadNetworkCommercialProjection(db, sources) {
|
|
7707
|
-
const stored = await
|
|
8108
|
+
const stored = await rows4(db, "networkCommercialEvents");
|
|
7708
8109
|
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7709
|
-
const persisted = await
|
|
8110
|
+
const persisted = await rows4(db, "networkCommercialExceptions");
|
|
7710
8111
|
for (const row of persisted) {
|
|
7711
8112
|
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7712
8113
|
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
@@ -7744,8 +8145,8 @@ function projectionOps(projection, now) {
|
|
|
7744
8145
|
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7745
8146
|
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7746
8147
|
const [existing, ...materialized] = await Promise.all([
|
|
7747
|
-
|
|
7748
|
-
...materializedNamespaces.map((ns) =>
|
|
8148
|
+
rows4(db, "networkCommercialExceptions"),
|
|
8149
|
+
...materializedNamespaces.map((ns) => rows4(db, ns))
|
|
7749
8150
|
]);
|
|
7750
8151
|
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7751
8152
|
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
@@ -7854,11 +8255,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
|
7854
8255
|
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7855
8256
|
throw new Error("Stripe returned an invalid commercial list response");
|
|
7856
8257
|
}
|
|
7857
|
-
const
|
|
7858
|
-
if (
|
|
7859
|
-
objects.push(...
|
|
8258
|
+
const rows5 = page.data.map(asRaw).filter((row) => row !== null);
|
|
8259
|
+
if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
8260
|
+
objects.push(...rows5);
|
|
7860
8261
|
if (!page.has_more) return { objects, complete: true };
|
|
7861
|
-
const cursor =
|
|
8262
|
+
const cursor = rows5.at(-1)?.id;
|
|
7862
8263
|
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7863
8264
|
startingAfter = cursor;
|
|
7864
8265
|
}
|
|
@@ -8110,14 +8511,14 @@ var PAGE2 = 500;
|
|
|
8110
8511
|
var MAX_SCAN = 5e3;
|
|
8111
8512
|
var MAX_RECORDS2 = 500;
|
|
8112
8513
|
async function localCommercialRows(db) {
|
|
8113
|
-
const
|
|
8514
|
+
const rows5 = [];
|
|
8114
8515
|
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
8115
8516
|
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
8116
8517
|
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
8117
|
-
|
|
8118
|
-
if (page.length < PAGE2) return { rows:
|
|
8518
|
+
rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
|
|
8519
|
+
if (page.length < PAGE2) return { rows: rows5, truncated: false };
|
|
8119
8520
|
}
|
|
8120
|
-
return { rows:
|
|
8521
|
+
return { rows: rows5, truncated: true };
|
|
8121
8522
|
}
|
|
8122
8523
|
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
8123
8524
|
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
@@ -8676,17 +9077,17 @@ async function builderLibraryDigest(document) {
|
|
|
8676
9077
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8677
9078
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8678
9079
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
8679
|
-
const
|
|
8680
|
-
return Array.isArray(
|
|
9080
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
9081
|
+
return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
|
|
8681
9082
|
}
|
|
8682
9083
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
8683
|
-
const
|
|
8684
|
-
return control2(Array.isArray(
|
|
9084
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
9085
|
+
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8685
9086
|
}
|
|
8686
9087
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
8687
9088
|
const id = `builder-library-local:${environment}`;
|
|
8688
|
-
const
|
|
8689
|
-
return local2(Array.isArray(
|
|
9089
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
9090
|
+
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
8690
9091
|
}
|
|
8691
9092
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
8692
9093
|
const { document, digest: digest2 } = envelope;
|