@odla-ai/chapter 0.47.4 → 0.47.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +317 -178
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +317 -178
- 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;
|
|
@@ -7013,7 +7013,7 @@ function canonicalNetworkPersonId(personId, persons) {
|
|
|
7013
7013
|
}
|
|
7014
7014
|
}
|
|
7015
7015
|
|
|
7016
|
-
// src/network-person-store.ts
|
|
7016
|
+
// src/network-person-store-state.ts
|
|
7017
7017
|
var NetworkPersonRegistryError = class extends Error {
|
|
7018
7018
|
constructor(code, message) {
|
|
7019
7019
|
super(message);
|
|
@@ -7021,29 +7021,112 @@ var NetworkPersonRegistryError = class extends Error {
|
|
|
7021
7021
|
}
|
|
7022
7022
|
code;
|
|
7023
7023
|
};
|
|
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 = {}) => ({
|
|
7024
|
+
var networkPersonEventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
7042
7025
|
t: "update",
|
|
7043
7026
|
ns: "networkPersonEvents",
|
|
7044
7027
|
id: `network_person_event:${mutationId}`,
|
|
7045
7028
|
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
7046
7029
|
});
|
|
7030
|
+
var asRows = (value) => Array.isArray(value) ? value : [];
|
|
7031
|
+
var MAX_ROWS = 1e4;
|
|
7032
|
+
async function boundedRows(db, ns, order2) {
|
|
7033
|
+
const collected = [];
|
|
7034
|
+
const pageSize = 500;
|
|
7035
|
+
for (let offset = 0; offset <= MAX_ROWS; offset += pageSize) {
|
|
7036
|
+
const result = await db.query({ [ns]: { $: { limit: pageSize, offset, ...order2 ? { order: order2 } : {} } } });
|
|
7037
|
+
const page = asRows(result[ns]);
|
|
7038
|
+
collected.push(...page);
|
|
7039
|
+
if (collected.length > MAX_ROWS) {
|
|
7040
|
+
throw new NetworkPersonRegistryError(
|
|
7041
|
+
"registry_too_large",
|
|
7042
|
+
`network Person registry ${ns} exceeds the ${MAX_ROWS.toLocaleString()}-row operational bound`
|
|
7043
|
+
);
|
|
7044
|
+
}
|
|
7045
|
+
if (page.length < pageSize) return collected;
|
|
7046
|
+
}
|
|
7047
|
+
return collected;
|
|
7048
|
+
}
|
|
7049
|
+
async function loadNetworkPersonRegistry(db) {
|
|
7050
|
+
const [persons, claims, aliases, links, events] = await Promise.all([
|
|
7051
|
+
boundedRows(db, "networkPersons", { updatedAt: "desc" }),
|
|
7052
|
+
boundedRows(db, "networkIdentityClaims"),
|
|
7053
|
+
boundedRows(db, "networkPersonAliases", { updatedAt: "desc" }),
|
|
7054
|
+
boundedRows(db, "networkChapterPersonLinks"),
|
|
7055
|
+
boundedRows(db, "networkPersonEvents", { createdAt: "desc" })
|
|
7056
|
+
]);
|
|
7057
|
+
return { persons, claims, aliases, links, events };
|
|
7058
|
+
}
|
|
7059
|
+
|
|
7060
|
+
// src/network-person-link-store.ts
|
|
7061
|
+
async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7062
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7063
|
+
const canonicalId = canonicalNetworkPersonId(input.personId, state.persons);
|
|
7064
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7065
|
+
if (!person || person.state !== "active" || canonicalId !== person.id) {
|
|
7066
|
+
throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
7067
|
+
}
|
|
7068
|
+
const sourceKey = await networkChapterPersonSourceKey(input.chapterId, input.chapterPersonId);
|
|
7069
|
+
const existingSource = state.links.find((link2) => link2.state === "shadow" && (link2.sourceKey === sourceKey || link2.chapterId === input.chapterId && link2.chapterPersonId === input.chapterPersonId));
|
|
7070
|
+
if (existingSource) {
|
|
7071
|
+
if (existingSource.personId !== person.id) {
|
|
7072
|
+
throw new NetworkPersonRegistryError("source_already_linked", "chapter source row is linked to another Person");
|
|
7073
|
+
}
|
|
7074
|
+
return { person, link: existingSource, replayed: true };
|
|
7075
|
+
}
|
|
7076
|
+
if (person.revision !== input.expectedPersonRevision) {
|
|
7077
|
+
throw new NetworkPersonRegistryError("stale_source_link", "Person changed; reload before linking this source row");
|
|
7078
|
+
}
|
|
7079
|
+
const expectedClaim = await networkIdentityClaim({ kind: "email", value: input.verifiedEmail, verifiedAt: 1 });
|
|
7080
|
+
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
7081
|
+
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
7082
|
+
}
|
|
7083
|
+
const id = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
7084
|
+
const previous = state.links.find((link2) => link2.id === id);
|
|
7085
|
+
const link = {
|
|
7086
|
+
id,
|
|
7087
|
+
personId: person.id,
|
|
7088
|
+
chapterId: input.chapterId,
|
|
7089
|
+
chapterPersonId: input.chapterPersonId,
|
|
7090
|
+
sourceKey,
|
|
7091
|
+
state: "shadow",
|
|
7092
|
+
createdAt: previous?.createdAt ?? now
|
|
7093
|
+
};
|
|
7094
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7095
|
+
await db.transact([
|
|
7096
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7097
|
+
{ t: "update", ns: "networkChapterPersonLinks", id, attrs: { ...link } },
|
|
7098
|
+
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
7099
|
+
], { mutationId, guards: [
|
|
7100
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7101
|
+
{ ns: "networkChapterPersonLinks", id, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
7102
|
+
] });
|
|
7103
|
+
return { person: nextPerson, link };
|
|
7104
|
+
}
|
|
7105
|
+
async function unlinkNetworkChapterPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7106
|
+
const reason = input.reason.trim();
|
|
7107
|
+
if (!reason || reason.length > 500) throw new NetworkPersonRegistryError("invalid_unlink", "a bounded unlink reason is required");
|
|
7108
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7109
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7110
|
+
const link = state.links.find((row) => row.id === input.linkId && row.personId === input.personId);
|
|
7111
|
+
if (!person || person.state !== "active" || person.revision !== input.expectedPersonRevision || !link || link.state !== "shadow") {
|
|
7112
|
+
throw new NetworkPersonRegistryError("stale_source_unlink", "Person or source link changed; reload before undoing the link");
|
|
7113
|
+
}
|
|
7114
|
+
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
7115
|
+
const nextLink = { ...link, state: "revoked" };
|
|
7116
|
+
delete nextLink.sourceKey;
|
|
7117
|
+
await db.transact([
|
|
7118
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
7119
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: link.id, attrs: { state: "revoked" } },
|
|
7120
|
+
{ t: "retract", ns: "networkChapterPersonLinks", id: link.id, attrs: ["sourceKey"] },
|
|
7121
|
+
networkPersonEventOp("chapter_source_unlinked", mutationId, actor, now, { chapterId: link.chapterId, reason }, { personId: person.id })
|
|
7122
|
+
], { mutationId, guards: [
|
|
7123
|
+
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
7124
|
+
{ ns: "networkChapterPersonLinks", id: link.id, exists: true, equals: { state: "shadow", personId: person.id } }
|
|
7125
|
+
] });
|
|
7126
|
+
return { person: nextPerson, link: nextLink };
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
// src/network-person-store.ts
|
|
7047
7130
|
var defaultPolicy = (now, consent) => ({
|
|
7048
7131
|
marketingSuppressed: consent.marketing !== "granted",
|
|
7049
7132
|
discretionarySuppressed: consent.discretionary !== "granted",
|
|
@@ -7053,7 +7136,8 @@ var isGuardConflict = (error) => error instanceof Error && error.code === "trans
|
|
|
7053
7136
|
async function linkOps(personId, links, now) {
|
|
7054
7137
|
return Promise.all(links.map(async (link) => {
|
|
7055
7138
|
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
7056
|
-
|
|
7139
|
+
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
7140
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
7057
7141
|
}));
|
|
7058
7142
|
}
|
|
7059
7143
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -7075,7 +7159,7 @@ async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId
|
|
|
7075
7159
|
};
|
|
7076
7160
|
await db.transact([
|
|
7077
7161
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
7078
|
-
|
|
7162
|
+
networkPersonEventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
7079
7163
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
7080
7164
|
return { status: "ambiguous", alias };
|
|
7081
7165
|
}
|
|
@@ -7111,7 +7195,7 @@ async function importNetworkPerson(db, input, mutationId, actor, now = Date.now(
|
|
|
7111
7195
|
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
7112
7196
|
...claimOps,
|
|
7113
7197
|
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
7114
|
-
|
|
7198
|
+
networkPersonEventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
7115
7199
|
];
|
|
7116
7200
|
try {
|
|
7117
7201
|
await db.transact(ops, { mutationId, guards: [
|
|
@@ -7133,12 +7217,21 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7133
7217
|
const state = await loadNetworkPersonRegistry(db);
|
|
7134
7218
|
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
7135
7219
|
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
7136
|
-
|
|
7220
|
+
const expectedStatus = input.decision === "reopen" ? "excluded" : "ambiguous";
|
|
7221
|
+
if (alias.revision !== input.expectedRevision || alias.status !== expectedStatus) throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
7222
|
+
if (input.decision === "reopen") {
|
|
7223
|
+
const next2 = { ...alias, status: "ambiguous", revision: alias.revision + 1, updatedAt: now };
|
|
7224
|
+
await db.transact([
|
|
7225
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7226
|
+
networkPersonEventOp("alias_reopened", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7227
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "excluded" } }] });
|
|
7228
|
+
return next2;
|
|
7229
|
+
}
|
|
7137
7230
|
if (input.decision === "exclude") {
|
|
7138
7231
|
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
7139
7232
|
await db.transact([
|
|
7140
7233
|
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7141
|
-
|
|
7234
|
+
networkPersonEventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7142
7235
|
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
7143
7236
|
return next2;
|
|
7144
7237
|
}
|
|
@@ -7154,7 +7247,7 @@ async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date
|
|
|
7154
7247
|
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7155
7248
|
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7156
7249
|
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7157
|
-
|
|
7250
|
+
networkPersonEventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7158
7251
|
], { mutationId, guards: [
|
|
7159
7252
|
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7160
7253
|
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
@@ -7184,7 +7277,7 @@ async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now(
|
|
|
7184
7277
|
await db.transact([
|
|
7185
7278
|
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7186
7279
|
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7187
|
-
|
|
7280
|
+
networkPersonEventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7188
7281
|
], { mutationId, guards: [
|
|
7189
7282
|
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7190
7283
|
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
@@ -7200,7 +7293,7 @@ async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now
|
|
|
7200
7293
|
await db.transact([
|
|
7201
7294
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7202
7295
|
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7203
|
-
|
|
7296
|
+
networkPersonEventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7204
7297
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7205
7298
|
return next;
|
|
7206
7299
|
}
|
|
@@ -7214,50 +7307,16 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
|
|
|
7214
7307
|
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7215
7308
|
await db.transact([
|
|
7216
7309
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7217
|
-
|
|
7310
|
+
networkPersonEventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7218
7311
|
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7219
7312
|
return next;
|
|
7220
7313
|
}
|
|
7221
7314
|
|
|
7222
|
-
// src/network-person-
|
|
7223
|
-
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7224
|
-
const reason = input.reason.trim();
|
|
7225
|
-
if (!reason || reason.length > 500) {
|
|
7226
|
-
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7227
|
-
}
|
|
7228
|
-
const state = await loadNetworkPersonRegistry(db);
|
|
7229
|
-
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7230
|
-
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7231
|
-
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7232
|
-
const event = {
|
|
7233
|
-
t: "update",
|
|
7234
|
-
ns: "networkPersonEvents",
|
|
7235
|
-
id: `network_person_event:${mutationId}`,
|
|
7236
|
-
attrs: {
|
|
7237
|
-
id: `network_person_event:${mutationId}`,
|
|
7238
|
-
kind: "pii_revealed",
|
|
7239
|
-
actorId: actor.id,
|
|
7240
|
-
...ids,
|
|
7241
|
-
mutationId,
|
|
7242
|
-
detail: { targetKind: input.kind, reason },
|
|
7243
|
-
createdAt: now
|
|
7244
|
-
}
|
|
7245
|
-
};
|
|
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
|
-
}
|
|
7252
|
-
|
|
7253
|
-
// src/worker-routes-network-person-registry.ts
|
|
7254
|
-
var PATH2 = "/api/admin/network-person-registry";
|
|
7255
|
-
var MAX_BODY2 = 32e3;
|
|
7256
|
-
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7315
|
+
// src/network-person-route-projection.ts
|
|
7257
7316
|
function redactedName(displayName) {
|
|
7258
7317
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7259
7318
|
}
|
|
7260
|
-
var
|
|
7319
|
+
var redactedNetworkPersonLink = async (link) => ({
|
|
7261
7320
|
id: link.id,
|
|
7262
7321
|
personId: link.personId,
|
|
7263
7322
|
chapterId: link.chapterId,
|
|
@@ -7265,13 +7324,13 @@ var redactedLink = async (link) => ({
|
|
|
7265
7324
|
state: link.state,
|
|
7266
7325
|
createdAt: link.createdAt
|
|
7267
7326
|
});
|
|
7268
|
-
var
|
|
7327
|
+
var safeNetworkContactPolicy = (policy) => ({
|
|
7269
7328
|
marketingSuppressed: policy.marketingSuppressed,
|
|
7270
7329
|
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7271
7330
|
hasReason: Boolean(policy.reason),
|
|
7272
7331
|
updatedAt: policy.updatedAt
|
|
7273
7332
|
});
|
|
7274
|
-
var
|
|
7333
|
+
var safeNetworkPersonAlias = async (alias) => ({
|
|
7275
7334
|
id: alias.id,
|
|
7276
7335
|
sourceSystem: alias.sourceSystem,
|
|
7277
7336
|
displayNameHint: redactedName(alias.displayName),
|
|
@@ -7286,17 +7345,91 @@ var safeAlias = (alias) => ({
|
|
|
7286
7345
|
},
|
|
7287
7346
|
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7288
7347
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7348
|
+
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
7349
|
+
chapterId: link.chapterId,
|
|
7350
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId)
|
|
7351
|
+
}))),
|
|
7289
7352
|
revision: alias.revision,
|
|
7290
7353
|
updatedAt: alias.updatedAt
|
|
7291
7354
|
});
|
|
7292
|
-
var
|
|
7355
|
+
var safeNetworkPerson = (person, claims = []) => ({
|
|
7293
7356
|
id: person.id,
|
|
7357
|
+
displayName: person.displayName,
|
|
7294
7358
|
displayNameHint: redactedName(person.displayName),
|
|
7359
|
+
verifiedEmails: claims.filter((claim) => claim.kind === "email").map((claim) => ({ value: claim.value, verifiedAt: claim.verifiedAt })).sort((left, right) => right.verifiedAt - left.verifiedAt || left.value.localeCompare(right.value)),
|
|
7295
7360
|
state: person.state,
|
|
7296
7361
|
mergedIntoId: person.mergedIntoId ?? null,
|
|
7297
7362
|
revision: person.revision,
|
|
7298
|
-
contactPolicy:
|
|
7363
|
+
contactPolicy: safeNetworkContactPolicy(person.contactPolicy)
|
|
7299
7364
|
});
|
|
7365
|
+
async function projectNetworkPersonRegistryState(state) {
|
|
7366
|
+
const persons = await Promise.all(state.persons.map(async (person) => {
|
|
7367
|
+
const claims = state.claims.filter((claim) => claim.personId === person.id);
|
|
7368
|
+
const links = state.links.filter((link) => link.personId === person.id && link.state === "shadow");
|
|
7369
|
+
const safeLinks = await Promise.all(links.map(redactedNetworkPersonLink));
|
|
7370
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7371
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7372
|
+
return {
|
|
7373
|
+
...safeNetworkPerson(person, claims),
|
|
7374
|
+
canonicalPersonId: canonicalId,
|
|
7375
|
+
identityClaims: claims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7376
|
+
chapterLinks: safeLinks,
|
|
7377
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
7378
|
+
};
|
|
7379
|
+
}));
|
|
7380
|
+
const aliases = await Promise.all(state.aliases.map(safeNetworkPersonAlias));
|
|
7381
|
+
return {
|
|
7382
|
+
persons,
|
|
7383
|
+
aliases,
|
|
7384
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7385
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7386
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7387
|
+
id: event.id,
|
|
7388
|
+
kind: event.kind,
|
|
7389
|
+
personId: event.personId ?? null,
|
|
7390
|
+
aliasId: event.aliasId ?? null,
|
|
7391
|
+
actorId: event.actorId,
|
|
7392
|
+
mutationId: event.mutationId,
|
|
7393
|
+
createdAt: event.createdAt
|
|
7394
|
+
}))
|
|
7395
|
+
};
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7398
|
+
// src/network-person-reveal.ts
|
|
7399
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7400
|
+
const reason = input.reason.trim();
|
|
7401
|
+
if (!reason || reason.length > 500) {
|
|
7402
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7403
|
+
}
|
|
7404
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7405
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7406
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7407
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7408
|
+
const event = {
|
|
7409
|
+
t: "update",
|
|
7410
|
+
ns: "networkPersonEvents",
|
|
7411
|
+
id: `network_person_event:${mutationId}`,
|
|
7412
|
+
attrs: {
|
|
7413
|
+
id: `network_person_event:${mutationId}`,
|
|
7414
|
+
kind: "pii_revealed",
|
|
7415
|
+
actorId: actor.id,
|
|
7416
|
+
...ids,
|
|
7417
|
+
mutationId,
|
|
7418
|
+
detail: { targetKind: input.kind, reason },
|
|
7419
|
+
createdAt: now
|
|
7420
|
+
}
|
|
7421
|
+
};
|
|
7422
|
+
await db.transact([event], {
|
|
7423
|
+
mutationId,
|
|
7424
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7425
|
+
});
|
|
7426
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7427
|
+
}
|
|
7428
|
+
|
|
7429
|
+
// src/worker-routes-network-person-registry.ts
|
|
7430
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7431
|
+
var MAX_BODY2 = 32e3;
|
|
7432
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7300
7433
|
var positiveRevision = (value) => {
|
|
7301
7434
|
const parsed = Number(value);
|
|
7302
7435
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
@@ -7352,14 +7485,14 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7352
7485
|
return json({
|
|
7353
7486
|
result: {
|
|
7354
7487
|
status: result.status,
|
|
7355
|
-
alias:
|
|
7356
|
-
..."person" in result && result.person ? { person:
|
|
7488
|
+
alias: await safeNetworkPersonAlias(result.alias),
|
|
7489
|
+
..."person" in result && result.person ? { person: safeNetworkPerson(result.person, result.alias.identityClaims) } : {},
|
|
7357
7490
|
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7358
7491
|
}
|
|
7359
7492
|
}, 201);
|
|
7360
7493
|
}
|
|
7361
7494
|
if (command === "review") {
|
|
7362
|
-
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or
|
|
7495
|
+
if (input.decision !== "link" && input.decision !== "exclude" && input.decision !== "reopen") return json({ error: "review decision must be link, exclude, or reopen" }, 400);
|
|
7363
7496
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7364
7497
|
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7365
7498
|
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
@@ -7369,7 +7502,42 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7369
7502
|
decision: input.decision,
|
|
7370
7503
|
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7371
7504
|
}, mutationId, actionActor);
|
|
7372
|
-
return json({ result:
|
|
7505
|
+
return json({ result: await safeNetworkPersonAlias(result) });
|
|
7506
|
+
}
|
|
7507
|
+
if (command === "link-source") {
|
|
7508
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7509
|
+
const chapterId = typeof input.chapterId === "string" ? input.chapterId : "";
|
|
7510
|
+
const chapterPersonId = typeof input.chapterPersonId === "string" ? input.chapterPersonId.trim() : "";
|
|
7511
|
+
const verifiedEmail = typeof input.verifiedEmail === "string" ? input.verifiedEmail.trim() : "";
|
|
7512
|
+
if (!expectedPersonRevision || !ctx.chapter.networkPersonRegistry.chapterIds.includes(chapterId) || !chapterPersonId || chapterPersonId.length > 200 || !verifiedEmail || verifiedEmail.length > 320) return json({ error: "a configured source row, exact email, and Person revision are required" }, 400);
|
|
7513
|
+
const result = await linkNetworkChapterPerson(db, {
|
|
7514
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7515
|
+
expectedPersonRevision,
|
|
7516
|
+
chapterId,
|
|
7517
|
+
chapterPersonId,
|
|
7518
|
+
verifiedEmail
|
|
7519
|
+
}, mutationId, actionActor);
|
|
7520
|
+
return json({
|
|
7521
|
+
result: {
|
|
7522
|
+
person: safeNetworkPerson(result.person),
|
|
7523
|
+
link: await redactedNetworkPersonLink(result.link),
|
|
7524
|
+
...result.replayed ? { replayed: true } : {}
|
|
7525
|
+
}
|
|
7526
|
+
});
|
|
7527
|
+
}
|
|
7528
|
+
if (command === "unlink-source") {
|
|
7529
|
+
const expectedPersonRevision = positiveRevision(input.expectedPersonRevision);
|
|
7530
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7531
|
+
if (!expectedPersonRevision || !reason || reason.length > 500) {
|
|
7532
|
+
return json({ error: "a Person revision and bounded unlink reason are required" }, 400);
|
|
7533
|
+
}
|
|
7534
|
+
const result = await unlinkNetworkChapterPerson(db, {
|
|
7535
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7536
|
+
linkId: typeof input.linkId === "string" ? input.linkId : "",
|
|
7537
|
+
expectedPersonRevision,
|
|
7538
|
+
reason
|
|
7539
|
+
}, mutationId, actionActor);
|
|
7540
|
+
return json({ result: { person: safeNetworkPerson(result.person), linkState: result.link.state } });
|
|
7373
7541
|
}
|
|
7374
7542
|
if (command === "merge") {
|
|
7375
7543
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7381,7 +7549,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7381
7549
|
expectedRevision,
|
|
7382
7550
|
expectedTargetRevision
|
|
7383
7551
|
}, mutationId, actionActor);
|
|
7384
|
-
return json({ result: { source:
|
|
7552
|
+
return json({ result: { source: safeNetworkPerson(result.source), target: safeNetworkPerson(result.target) } });
|
|
7385
7553
|
}
|
|
7386
7554
|
if (command === "unmerge") {
|
|
7387
7555
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7390,7 +7558,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7390
7558
|
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7391
7559
|
expectedRevision
|
|
7392
7560
|
}, mutationId, actionActor);
|
|
7393
|
-
return json({ result:
|
|
7561
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7394
7562
|
}
|
|
7395
7563
|
if (command === "suppression") {
|
|
7396
7564
|
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
@@ -7406,7 +7574,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7406
7574
|
discretionarySuppressed: input.discretionarySuppressed,
|
|
7407
7575
|
...reason ? { reason } : {}
|
|
7408
7576
|
}, mutationId, actionActor);
|
|
7409
|
-
return json({ result:
|
|
7577
|
+
return json({ result: safeNetworkPerson(result) });
|
|
7410
7578
|
}
|
|
7411
7579
|
if (command === "reveal") {
|
|
7412
7580
|
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
@@ -7425,48 +7593,19 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7425
7593
|
}
|
|
7426
7594
|
}
|
|
7427
7595
|
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);
|
|
7596
|
+
const projection = await projectNetworkPersonRegistryState(state);
|
|
7447
7597
|
return json({
|
|
7448
7598
|
version: 1,
|
|
7449
7599
|
mode: "shadow",
|
|
7450
7600
|
writable: true,
|
|
7451
7601
|
authoritativeForChapterOperation: false,
|
|
7452
|
-
//
|
|
7453
|
-
//
|
|
7454
|
-
|
|
7455
|
-
|
|
7602
|
+
// The store pages every collection to its explicit 10,000-row operational
|
|
7603
|
+
// bound and rejects larger state instead of returning a partial projection.
|
|
7604
|
+
chapterLinkProjectionComplete: true,
|
|
7605
|
+
personProjectionComplete: true,
|
|
7606
|
+
aliasProjectionComplete: true,
|
|
7456
7607
|
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
|
-
}))
|
|
7608
|
+
...projection
|
|
7470
7609
|
});
|
|
7471
7610
|
};
|
|
7472
7611
|
|
|
@@ -7659,12 +7798,12 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
7659
7798
|
const prior = latest.get(key);
|
|
7660
7799
|
if (!prior || order(prior, fact) <= 0) latest.set(key, row);
|
|
7661
7800
|
}
|
|
7662
|
-
const
|
|
7663
|
-
const customers =
|
|
7664
|
-
const subscriptions =
|
|
7665
|
-
const invoices =
|
|
7666
|
-
const refunds =
|
|
7667
|
-
for (const charge of
|
|
7801
|
+
const rows5 = [...latest.values()];
|
|
7802
|
+
const customers = rows5.filter((row) => row.objectKind === "customer");
|
|
7803
|
+
const subscriptions = rows5.filter((row) => row.objectKind === "subscription");
|
|
7804
|
+
const invoices = rows5.filter((row) => row.objectKind === "invoice");
|
|
7805
|
+
const refunds = rows5.filter((row) => row.objectKind === "refund" && row.eventType.startsWith("refund."));
|
|
7806
|
+
for (const charge of rows5.filter((row) => row.eventType === "charge.refunded")) {
|
|
7668
7807
|
if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
|
|
7669
7808
|
exceptions.push({ eventId: charge.eventId, kind: "charge_refund_without_refund_object", sourceKey: charge.sourceKey, detail: { eventType: charge.eventType, objectId: charge.objectId } });
|
|
7670
7809
|
}
|
|
@@ -7693,7 +7832,7 @@ var materializedNamespaces = [
|
|
|
7693
7832
|
"networkCommercialInvoices",
|
|
7694
7833
|
"networkCommercialRefunds"
|
|
7695
7834
|
];
|
|
7696
|
-
async function
|
|
7835
|
+
async function rows4(db, ns) {
|
|
7697
7836
|
const all = [];
|
|
7698
7837
|
const pageSize = 500;
|
|
7699
7838
|
for (let offset = 0; ; offset += pageSize) {
|
|
@@ -7704,9 +7843,9 @@ async function rows5(db, ns) {
|
|
|
7704
7843
|
}
|
|
7705
7844
|
}
|
|
7706
7845
|
async function loadNetworkCommercialProjection(db, sources) {
|
|
7707
|
-
const stored = await
|
|
7846
|
+
const stored = await rows4(db, "networkCommercialEvents");
|
|
7708
7847
|
const projection = projectNetworkCommercialEvents(stored.map((row) => row.fact), sources);
|
|
7709
|
-
const persisted = await
|
|
7848
|
+
const persisted = await rows4(db, "networkCommercialExceptions");
|
|
7710
7849
|
for (const row of persisted) {
|
|
7711
7850
|
if (row.status !== "active" || row.kind !== "collision") continue;
|
|
7712
7851
|
if (projection.exceptions.some((item) => item.eventId === row.eventId && item.kind === "collision")) continue;
|
|
@@ -7744,8 +7883,8 @@ function projectionOps(projection, now) {
|
|
|
7744
7883
|
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
7745
7884
|
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
7746
7885
|
const [existing, ...materialized] = await Promise.all([
|
|
7747
|
-
|
|
7748
|
-
...materializedNamespaces.map((ns) =>
|
|
7886
|
+
rows4(db, "networkCommercialExceptions"),
|
|
7887
|
+
...materializedNamespaces.map((ns) => rows4(db, ns))
|
|
7749
7888
|
]);
|
|
7750
7889
|
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
7751
7890
|
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
@@ -7854,11 +7993,11 @@ async function listStripeObjects(client, path, params = {}, maxPages = 20) {
|
|
|
7854
7993
|
if (!Array.isArray(page.data) || typeof page.has_more !== "boolean") {
|
|
7855
7994
|
throw new Error("Stripe returned an invalid commercial list response");
|
|
7856
7995
|
}
|
|
7857
|
-
const
|
|
7858
|
-
if (
|
|
7859
|
-
objects.push(...
|
|
7996
|
+
const rows5 = page.data.map(asRaw).filter((row) => row !== null);
|
|
7997
|
+
if (rows5.length !== page.data.length) throw new Error("Stripe returned a malformed commercial object");
|
|
7998
|
+
objects.push(...rows5);
|
|
7860
7999
|
if (!page.has_more) return { objects, complete: true };
|
|
7861
|
-
const cursor =
|
|
8000
|
+
const cursor = rows5.at(-1)?.id;
|
|
7862
8001
|
if (typeof cursor !== "string" || !cursor) throw new Error("Stripe pagination did not provide a cursor");
|
|
7863
8002
|
startingAfter = cursor;
|
|
7864
8003
|
}
|
|
@@ -8110,14 +8249,14 @@ var PAGE2 = 500;
|
|
|
8110
8249
|
var MAX_SCAN = 5e3;
|
|
8111
8250
|
var MAX_RECORDS2 = 500;
|
|
8112
8251
|
async function localCommercialRows(db) {
|
|
8113
|
-
const
|
|
8252
|
+
const rows5 = [];
|
|
8114
8253
|
for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
|
|
8115
8254
|
const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
|
|
8116
8255
|
const page = Array.isArray(result.applications) ? result.applications : [];
|
|
8117
|
-
|
|
8118
|
-
if (page.length < PAGE2) return { rows:
|
|
8256
|
+
rows5.push(...page.slice(0, MAX_SCAN - rows5.length));
|
|
8257
|
+
if (page.length < PAGE2) return { rows: rows5, truncated: false };
|
|
8119
8258
|
}
|
|
8120
|
-
return { rows:
|
|
8259
|
+
return { rows: rows5, truncated: true };
|
|
8121
8260
|
}
|
|
8122
8261
|
var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
8123
8262
|
if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
|
|
@@ -8676,17 +8815,17 @@ async function builderLibraryDigest(document) {
|
|
|
8676
8815
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8677
8816
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
8678
8817
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
8679
|
-
const
|
|
8680
|
-
return Array.isArray(
|
|
8818
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
8819
|
+
return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
|
|
8681
8820
|
}
|
|
8682
8821
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
8683
|
-
const
|
|
8684
|
-
return control2(Array.isArray(
|
|
8822
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
8823
|
+
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
8685
8824
|
}
|
|
8686
8825
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
8687
8826
|
const id = `builder-library-local:${environment}`;
|
|
8688
|
-
const
|
|
8689
|
-
return local2(Array.isArray(
|
|
8827
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
8828
|
+
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
8690
8829
|
}
|
|
8691
8830
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
8692
8831
|
const { document, digest: digest2 } = envelope;
|