@odla-ai/chapter 0.43.1 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{copy-context-Bh1EZvCg.d.ts → copy-context-B4SCy6vx.d.ts} +18 -0
- package/dist/index.cjs +214 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +214 -102
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +653 -61
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +18 -0
- package/dist/worker/index.d.ts +18 -0
- package/dist/worker/index.js +653 -61
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -336,8 +336,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
336
336
|
function byOrder(a, b) {
|
|
337
337
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
338
338
|
}
|
|
339
|
-
function resolveTiers(group,
|
|
340
|
-
const stored =
|
|
339
|
+
function resolveTiers(group, rows5 = []) {
|
|
340
|
+
const stored = rows5.filter((tier) => tier.active);
|
|
341
341
|
if (stored.length) return [...stored].sort(byOrder);
|
|
342
342
|
return [{
|
|
343
343
|
id: "standard",
|
|
@@ -383,15 +383,15 @@ function rowToTier(row, groupId) {
|
|
|
383
383
|
}
|
|
384
384
|
async function loadTiers(db, group) {
|
|
385
385
|
const groupId = String(group.id ?? "");
|
|
386
|
-
let
|
|
386
|
+
let rows5;
|
|
387
387
|
try {
|
|
388
|
-
|
|
388
|
+
rows5 = (await db.query({
|
|
389
389
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
390
390
|
})).tiers;
|
|
391
391
|
} catch {
|
|
392
|
-
|
|
392
|
+
rows5 = void 0;
|
|
393
393
|
}
|
|
394
|
-
const list = Array.isArray(
|
|
394
|
+
const list = Array.isArray(rows5) ? rows5.map((row) => rowToTier(row, groupId)) : [];
|
|
395
395
|
return resolveTiers(group, list);
|
|
396
396
|
}
|
|
397
397
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -1450,27 +1450,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1450
1450
|
if (db.aggregate) {
|
|
1451
1451
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1452
1452
|
}
|
|
1453
|
-
const
|
|
1454
|
-
return Array.isArray(
|
|
1453
|
+
const rows5 = (await db.query({ signupControls: {} })).signupControls;
|
|
1454
|
+
return Array.isArray(rows5) && rows5.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1455
1455
|
}
|
|
1456
1456
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1457
1457
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1458
|
-
const
|
|
1458
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1459
1459
|
where: { targetId, environment },
|
|
1460
1460
|
order: { revision: "desc" },
|
|
1461
1461
|
limit: 1
|
|
1462
1462
|
} } })).signupControls;
|
|
1463
|
-
const latest = Array.isArray(
|
|
1463
|
+
const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1464
1464
|
return asStored(latest);
|
|
1465
1465
|
}
|
|
1466
1466
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1467
1467
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1468
|
-
const
|
|
1468
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1469
1469
|
where: { targetId, environment, active: true },
|
|
1470
1470
|
order: { revision: "desc" },
|
|
1471
1471
|
limit: 1
|
|
1472
1472
|
} } })).signupControls;
|
|
1473
|
-
const active = Array.isArray(
|
|
1473
|
+
const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1474
1474
|
return asStored(active);
|
|
1475
1475
|
}
|
|
1476
1476
|
function controlledApplication(base, document) {
|
|
@@ -2846,33 +2846,33 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2846
2846
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2847
2847
|
async function exists(db, ns, where) {
|
|
2848
2848
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2849
|
-
const
|
|
2850
|
-
return
|
|
2849
|
+
const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2850
|
+
return rows5.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2851
2851
|
}
|
|
2852
2852
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2853
2853
|
const where = { targetId, environment, active: true };
|
|
2854
2854
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2855
|
-
const
|
|
2855
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2856
2856
|
where,
|
|
2857
2857
|
order: { revision: "desc" },
|
|
2858
2858
|
limit: 1
|
|
2859
2859
|
} } })).memberContentControls;
|
|
2860
|
-
return control(Array.isArray(
|
|
2860
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2861
2861
|
}
|
|
2862
2862
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2863
2863
|
const where = { targetId, environment };
|
|
2864
2864
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2865
|
-
const
|
|
2865
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2866
2866
|
where,
|
|
2867
2867
|
order: { revision: "desc" },
|
|
2868
2868
|
limit: 1
|
|
2869
2869
|
} } })).memberContentControls;
|
|
2870
|
-
return control(Array.isArray(
|
|
2870
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2871
2871
|
}
|
|
2872
2872
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2873
2873
|
const id = `member-content-local:${environment}`;
|
|
2874
|
-
const
|
|
2875
|
-
return local(Array.isArray(
|
|
2874
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2875
|
+
return local(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
2876
2876
|
}
|
|
2877
2877
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2878
2878
|
const { document, digest } = envelope;
|
|
@@ -2927,12 +2927,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2927
2927
|
}
|
|
2928
2928
|
async function memberTierId(db, chapterId, email) {
|
|
2929
2929
|
if (!email) return null;
|
|
2930
|
-
let
|
|
2930
|
+
let rows5 = (await db.query({ applications: { $: {
|
|
2931
2931
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2932
2932
|
limit: 20
|
|
2933
2933
|
} } })).applications;
|
|
2934
|
-
if (!
|
|
2935
|
-
const latest = Array.isArray(
|
|
2934
|
+
if (!rows5?.length) rows5 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2935
|
+
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;
|
|
2936
2936
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2937
2937
|
}
|
|
2938
2938
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -3658,8 +3658,8 @@ function makeCalendar(env) {
|
|
|
3658
3658
|
}
|
|
3659
3659
|
async function firstRow(db, ns, q) {
|
|
3660
3660
|
const res = await db.query({ [ns]: { $: q } });
|
|
3661
|
-
const
|
|
3662
|
-
return Array.isArray(
|
|
3661
|
+
const rows5 = res[ns];
|
|
3662
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
3663
3663
|
}
|
|
3664
3664
|
async function computeSlots(cal, cfg) {
|
|
3665
3665
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -4059,8 +4059,8 @@ function createStripeProvider(config) {
|
|
|
4059
4059
|
const sk = requireSecret(secretKey);
|
|
4060
4060
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4061
4061
|
if (!charges.ok) fail("charges list", charges);
|
|
4062
|
-
const
|
|
4063
|
-
const paid =
|
|
4062
|
+
const rows5 = charges.body.data ?? [];
|
|
4063
|
+
const paid = rows5.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4064
4064
|
const charge = paid[paid.length - 1];
|
|
4065
4065
|
if (!charge) {
|
|
4066
4066
|
const err = new Error("no paid charge to refund");
|
|
@@ -4168,8 +4168,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4168
4168
|
throw new Error("gift issuance conflict");
|
|
4169
4169
|
}
|
|
4170
4170
|
}
|
|
4171
|
-
function buyerGiftClaims(
|
|
4172
|
-
return
|
|
4171
|
+
function buyerGiftClaims(rows5, now) {
|
|
4172
|
+
return rows5.map((row) => {
|
|
4173
4173
|
const stored = String(row.status ?? "pending");
|
|
4174
4174
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4175
4175
|
return {
|
|
@@ -4461,8 +4461,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4461
4461
|
|
|
4462
4462
|
// src/stripe-webhook-partition.ts
|
|
4463
4463
|
var firstRow2 = async (db, ns, q) => {
|
|
4464
|
-
const
|
|
4465
|
-
return Array.isArray(
|
|
4464
|
+
const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4465
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4466
4466
|
};
|
|
4467
4467
|
var stripeEventDigest = async (rawBody) => {
|
|
4468
4468
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4532,8 +4532,8 @@ async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
|
4532
4532
|
// src/worker-routes-payments.ts
|
|
4533
4533
|
async function firstRow3(db, ns, q) {
|
|
4534
4534
|
const res = await db.query({ [ns]: { $: q } });
|
|
4535
|
-
const
|
|
4536
|
-
return Array.isArray(
|
|
4535
|
+
const rows5 = res[ns];
|
|
4536
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4537
4537
|
}
|
|
4538
4538
|
async function findApplication(db, event) {
|
|
4539
4539
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -4999,8 +4999,8 @@ async function upcomingEvents(env) {
|
|
|
4999
4999
|
const res = await cal.availability.upcoming();
|
|
5000
5000
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5001
5001
|
}
|
|
5002
|
-
function toReconcile(
|
|
5003
|
-
return
|
|
5002
|
+
function toReconcile(rows5) {
|
|
5003
|
+
return rows5.map((m) => ({
|
|
5004
5004
|
id: String(m.id),
|
|
5005
5005
|
applicationId: String(m.applicationId),
|
|
5006
5006
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5022,12 +5022,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5022
5022
|
const query = await db.query({
|
|
5023
5023
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5024
5024
|
});
|
|
5025
|
-
let
|
|
5026
|
-
if (Number.isFinite(from))
|
|
5027
|
-
if (Number.isFinite(to))
|
|
5025
|
+
let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5026
|
+
if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5027
|
+
if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5028
5028
|
let decisions = [];
|
|
5029
5029
|
try {
|
|
5030
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5030
|
+
decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
|
|
5031
5031
|
} catch {
|
|
5032
5032
|
}
|
|
5033
5033
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5054,7 +5054,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5054
5054
|
};
|
|
5055
5055
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5056
5056
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5057
|
-
const meetings =
|
|
5057
|
+
const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5058
5058
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5059
5059
|
};
|
|
5060
5060
|
|
|
@@ -5075,16 +5075,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
5075
5075
|
});
|
|
5076
5076
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
5077
5077
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
5078
|
-
const
|
|
5079
|
-
if (
|
|
5080
|
-
const result = await backfillCrm(crmDeps2(
|
|
5078
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5079
|
+
if (gate11 instanceof Response) return gate11;
|
|
5080
|
+
const result = await backfillCrm(crmDeps2(gate11.db, ctx));
|
|
5081
5081
|
return json({ ok: true, ...result });
|
|
5082
5082
|
};
|
|
5083
5083
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
5084
5084
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
5085
|
-
const
|
|
5086
|
-
if (
|
|
5087
|
-
const { db } =
|
|
5085
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5086
|
+
if (gate11 instanceof Response) return gate11;
|
|
5087
|
+
const { db } = gate11;
|
|
5088
5088
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
5089
5089
|
const [data, roleList] = await Promise.all([
|
|
5090
5090
|
db.query({
|
|
@@ -5130,16 +5130,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5130
5130
|
});
|
|
5131
5131
|
}
|
|
5132
5132
|
}
|
|
5133
|
-
const
|
|
5133
|
+
const rows5 = [...people.values()].sort(
|
|
5134
5134
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5135
5135
|
);
|
|
5136
|
-
return json({ people:
|
|
5136
|
+
return json({ people: rows5 });
|
|
5137
5137
|
};
|
|
5138
5138
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5139
5139
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
5140
|
-
const
|
|
5141
|
-
if (
|
|
5142
|
-
const { db } =
|
|
5140
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5141
|
+
if (gate11 instanceof Response) return gate11;
|
|
5142
|
+
const { db } = gate11;
|
|
5143
5143
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
5144
5144
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
5145
5145
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -5150,9 +5150,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
5150
5150
|
};
|
|
5151
5151
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
5152
5152
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
5153
|
-
const
|
|
5154
|
-
if (
|
|
5155
|
-
const { db, actor } =
|
|
5153
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5154
|
+
if (gate11 instanceof Response) return gate11;
|
|
5155
|
+
const { db, actor } = gate11;
|
|
5156
5156
|
let body;
|
|
5157
5157
|
try {
|
|
5158
5158
|
body = await req.json();
|
|
@@ -6725,6 +6725,596 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6725
6725
|
}
|
|
6726
6726
|
};
|
|
6727
6727
|
|
|
6728
|
+
// src/network-person-registry.ts
|
|
6729
|
+
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6730
|
+
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6731
|
+
function normalizeNetworkIdentity(kind, value) {
|
|
6732
|
+
const trimmed = value.trim();
|
|
6733
|
+
if (kind === "email") return trimmed.toLowerCase();
|
|
6734
|
+
if (kind === "phone") return trimmed.replace(/[\s().-]/g, "");
|
|
6735
|
+
return trimmed;
|
|
6736
|
+
}
|
|
6737
|
+
function emailLooksReal(value) {
|
|
6738
|
+
const at = value.lastIndexOf("@");
|
|
6739
|
+
if (at <= 0 || at === value.length - 1) return false;
|
|
6740
|
+
const domain = value.slice(at + 1).toLowerCase();
|
|
6741
|
+
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
6742
|
+
}
|
|
6743
|
+
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
6744
|
+
const errors = [];
|
|
6745
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, errors: ["import must be an object"] };
|
|
6746
|
+
const raw = input;
|
|
6747
|
+
if (raw.sourceKind !== "external_reimport") errors.push("sourceKind must be external_reimport");
|
|
6748
|
+
if (raw.approved !== true || raw.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
6749
|
+
const sourceSystem = typeof raw.sourceSystem === "string" ? raw.sourceSystem.trim().toLowerCase() : "";
|
|
6750
|
+
const sourceRecordId = typeof raw.sourceRecordId === "string" ? raw.sourceRecordId.trim() : "";
|
|
6751
|
+
const provenance = typeof raw.provenance === "string" ? raw.provenance.trim() : "";
|
|
6752
|
+
const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
|
|
6753
|
+
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
6754
|
+
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6755
|
+
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6756
|
+
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6757
|
+
const rawConsent = raw.consent && typeof raw.consent === "object" && !Array.isArray(raw.consent) ? raw.consent : {};
|
|
6758
|
+
const consentStates = ["unknown", "granted", "denied"];
|
|
6759
|
+
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
6760
|
+
const discretionary = typeof rawConsent.discretionary === "string" ? rawConsent.discretionary : "";
|
|
6761
|
+
const recordedAt = Number(rawConsent.recordedAt);
|
|
6762
|
+
const basis = typeof rawConsent.basis === "string" ? rawConsent.basis.trim() : "";
|
|
6763
|
+
if (!consentStates.includes(marketing) || !consentStates.includes(discretionary) || !Number.isSafeInteger(recordedAt) || recordedAt <= 0 || !basis || basis.length > 500) {
|
|
6764
|
+
errors.push("consent must include marketing and discretionary states, recordedAt, and a bounded basis");
|
|
6765
|
+
}
|
|
6766
|
+
const identities = [];
|
|
6767
|
+
if (!Array.isArray(raw.identities) || raw.identities.length < 1 || raw.identities.length > 8) {
|
|
6768
|
+
errors.push("one to eight verified identities are required");
|
|
6769
|
+
} else {
|
|
6770
|
+
for (const item of raw.identities) {
|
|
6771
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6772
|
+
errors.push("identity must be an object");
|
|
6773
|
+
continue;
|
|
6774
|
+
}
|
|
6775
|
+
const candidate = item;
|
|
6776
|
+
const kind = candidate.kind;
|
|
6777
|
+
const value = typeof candidate.value === "string" ? candidate.value : "";
|
|
6778
|
+
const verifiedAt = Number(candidate.verifiedAt);
|
|
6779
|
+
if (kind !== "email" && kind !== "phone" && kind !== "external") {
|
|
6780
|
+
errors.push("identity kind is invalid");
|
|
6781
|
+
continue;
|
|
6782
|
+
}
|
|
6783
|
+
const normalized = normalizeNetworkIdentity(kind, value);
|
|
6784
|
+
if (!normalized || normalized.length > 320 || !Number.isSafeInteger(verifiedAt) || verifiedAt <= 0) {
|
|
6785
|
+
errors.push("identity value and verifiedAt are required and bounded");
|
|
6786
|
+
continue;
|
|
6787
|
+
}
|
|
6788
|
+
if (kind === "email" && !emailLooksReal(normalized)) {
|
|
6789
|
+
errors.push("development or invalid email identities cannot become Persons");
|
|
6790
|
+
continue;
|
|
6791
|
+
}
|
|
6792
|
+
identities.push({ kind, value: normalized, verifiedAt });
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6796
|
+
for (const identity of identities) {
|
|
6797
|
+
const key = `${identity.kind}:${identity.value}`;
|
|
6798
|
+
if (seen.has(key)) errors.push("duplicate identity in import");
|
|
6799
|
+
seen.add(key);
|
|
6800
|
+
}
|
|
6801
|
+
const chapterLinks = [];
|
|
6802
|
+
if (raw.chapterLinks !== void 0) {
|
|
6803
|
+
if (!Array.isArray(raw.chapterLinks) || raw.chapterLinks.length > allowedChapterIds.length) errors.push("chapterLinks are invalid or too numerous");
|
|
6804
|
+
else for (const item of raw.chapterLinks) {
|
|
6805
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6806
|
+
errors.push("chapter link must be an object");
|
|
6807
|
+
continue;
|
|
6808
|
+
}
|
|
6809
|
+
const chapterId = typeof item.chapterId === "string" ? String(item.chapterId) : "";
|
|
6810
|
+
const chapterPersonId = typeof item.chapterPersonId === "string" ? String(item.chapterPersonId).trim() : "";
|
|
6811
|
+
if (!CHAPTER_ID.test(chapterId) || !allowedChapterIds.includes(chapterId) || !chapterPersonId || chapterPersonId.length > 200) errors.push("chapter link is outside the configured network");
|
|
6812
|
+
else chapterLinks.push({ chapterId, chapterPersonId });
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
if (errors.length) return { ok: false, errors };
|
|
6816
|
+
return { ok: true, errors: [], value: {
|
|
6817
|
+
sourceKind: "external_reimport",
|
|
6818
|
+
sourceSystem,
|
|
6819
|
+
sourceRecordId,
|
|
6820
|
+
provenance,
|
|
6821
|
+
approved: true,
|
|
6822
|
+
isRealPerson: true,
|
|
6823
|
+
displayName,
|
|
6824
|
+
consent: {
|
|
6825
|
+
marketing,
|
|
6826
|
+
discretionary,
|
|
6827
|
+
recordedAt,
|
|
6828
|
+
basis
|
|
6829
|
+
},
|
|
6830
|
+
identities,
|
|
6831
|
+
...chapterLinks.length ? { chapterLinks } : {}
|
|
6832
|
+
} };
|
|
6833
|
+
}
|
|
6834
|
+
async function sha256(value) {
|
|
6835
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
6836
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
6837
|
+
}
|
|
6838
|
+
async function networkIdentityClaim(input) {
|
|
6839
|
+
const normalized = normalizeNetworkIdentity(input.kind, input.value);
|
|
6840
|
+
const digest = await sha256(`${input.kind}\0${normalized}`);
|
|
6841
|
+
const hint = input.kind === "email" ? `${normalized.slice(0, 1)}***@${normalized.split("@")[1]}` : input.kind === "phone" ? `***${normalized.slice(-4)}` : void 0;
|
|
6842
|
+
return { id: `identity_${digest}`, kind: input.kind, value: normalized, ...hint ? { hint } : {}, verifiedAt: input.verifiedAt };
|
|
6843
|
+
}
|
|
6844
|
+
async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
6845
|
+
return `alias_${await sha256(`${sourceSystem}\0${sourceRecordId}`)}`;
|
|
6846
|
+
}
|
|
6847
|
+
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6848
|
+
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6849
|
+
}
|
|
6850
|
+
function projectNetworkContactPolicy(policy, links) {
|
|
6851
|
+
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6852
|
+
}
|
|
6853
|
+
function canonicalNetworkPersonId(personId, persons) {
|
|
6854
|
+
const byId = new Map(persons.map((person) => [person.id, person]));
|
|
6855
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6856
|
+
let current = personId;
|
|
6857
|
+
while (true) {
|
|
6858
|
+
if (seen.has(current)) return null;
|
|
6859
|
+
seen.add(current);
|
|
6860
|
+
const person = byId.get(current);
|
|
6861
|
+
if (!person) return null;
|
|
6862
|
+
if (!person.mergedIntoId) return person.id;
|
|
6863
|
+
current = person.mergedIntoId;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6867
|
+
// src/network-person-store.ts
|
|
6868
|
+
var NetworkPersonRegistryError = class extends Error {
|
|
6869
|
+
constructor(code, message) {
|
|
6870
|
+
super(message);
|
|
6871
|
+
this.code = code;
|
|
6872
|
+
}
|
|
6873
|
+
code;
|
|
6874
|
+
};
|
|
6875
|
+
var rows4 = (value) => Array.isArray(value) ? value : [];
|
|
6876
|
+
async function loadNetworkPersonRegistry(db) {
|
|
6877
|
+
const result = await db.query({
|
|
6878
|
+
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6879
|
+
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
6880
|
+
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6881
|
+
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
6882
|
+
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
6883
|
+
});
|
|
6884
|
+
return {
|
|
6885
|
+
persons: rows4(result.networkPersons),
|
|
6886
|
+
claims: rows4(result.networkIdentityClaims),
|
|
6887
|
+
aliases: rows4(result.networkPersonAliases),
|
|
6888
|
+
links: rows4(result.networkChapterPersonLinks),
|
|
6889
|
+
events: rows4(result.networkPersonEvents)
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
6893
|
+
t: "update",
|
|
6894
|
+
ns: "networkPersonEvents",
|
|
6895
|
+
id: `network_person_event:${mutationId}`,
|
|
6896
|
+
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
6897
|
+
});
|
|
6898
|
+
var defaultPolicy = (now, consent) => ({
|
|
6899
|
+
marketingSuppressed: consent.marketing !== "granted",
|
|
6900
|
+
discretionarySuppressed: consent.discretionary !== "granted",
|
|
6901
|
+
updatedAt: now
|
|
6902
|
+
});
|
|
6903
|
+
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
6904
|
+
async function linkOps(personId, links, now) {
|
|
6905
|
+
return Promise.all(links.map(async (link) => {
|
|
6906
|
+
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
6907
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, state: "shadow", createdAt: now } };
|
|
6908
|
+
}));
|
|
6909
|
+
}
|
|
6910
|
+
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
6911
|
+
const alias = {
|
|
6912
|
+
id: aliasId,
|
|
6913
|
+
sourceSystem: input.sourceSystem,
|
|
6914
|
+
sourceRecordId: input.sourceRecordId,
|
|
6915
|
+
sourceKey: aliasId,
|
|
6916
|
+
displayName: input.displayName,
|
|
6917
|
+
provenance: input.provenance,
|
|
6918
|
+
consent: input.consent,
|
|
6919
|
+
status: "ambiguous",
|
|
6920
|
+
candidatePersonIds: candidates,
|
|
6921
|
+
identityClaims: claims,
|
|
6922
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6923
|
+
revision: 1,
|
|
6924
|
+
createdAt: now,
|
|
6925
|
+
updatedAt: now
|
|
6926
|
+
};
|
|
6927
|
+
await db.transact([
|
|
6928
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
6929
|
+
eventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
6930
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
6931
|
+
return { status: "ambiguous", alias };
|
|
6932
|
+
}
|
|
6933
|
+
async function importNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
6934
|
+
const aliasId = await networkSourceKey(input.sourceSystem, input.sourceRecordId);
|
|
6935
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
6936
|
+
const replay = state.aliases.find((alias2) => alias2.id === aliasId);
|
|
6937
|
+
if (replay) return { status: replay.status, alias: replay, replayed: true };
|
|
6938
|
+
const claims = await Promise.all(input.identities.map(networkIdentityClaim));
|
|
6939
|
+
const candidates = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
6940
|
+
if (candidates.length) return ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now);
|
|
6941
|
+
const personId = `person_${aliasId.slice("alias_".length)}`;
|
|
6942
|
+
const person = { id: personId, displayName: input.displayName, state: "active", contactPolicy: defaultPolicy(now, input.consent), revision: 1, createdAt: now, updatedAt: now };
|
|
6943
|
+
const alias = {
|
|
6944
|
+
id: aliasId,
|
|
6945
|
+
sourceSystem: input.sourceSystem,
|
|
6946
|
+
sourceRecordId: input.sourceRecordId,
|
|
6947
|
+
sourceKey: aliasId,
|
|
6948
|
+
displayName: input.displayName,
|
|
6949
|
+
provenance: input.provenance,
|
|
6950
|
+
consent: input.consent,
|
|
6951
|
+
status: "linked",
|
|
6952
|
+
personId,
|
|
6953
|
+
identityClaims: claims,
|
|
6954
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6955
|
+
revision: 1,
|
|
6956
|
+
createdAt: now,
|
|
6957
|
+
updatedAt: now
|
|
6958
|
+
};
|
|
6959
|
+
const claimOps = claims.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId, createdAt: now } }));
|
|
6960
|
+
const ops = [
|
|
6961
|
+
{ t: "update", ns: "networkPersons", id: personId, attrs: { ...person } },
|
|
6962
|
+
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
6963
|
+
...claimOps,
|
|
6964
|
+
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
6965
|
+
eventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
6966
|
+
];
|
|
6967
|
+
try {
|
|
6968
|
+
await db.transact(ops, { mutationId, guards: [
|
|
6969
|
+
{ ns: "networkPersonAliases", id: aliasId, exists: false },
|
|
6970
|
+
...claims.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
6971
|
+
] });
|
|
6972
|
+
return { status: "linked", alias, person };
|
|
6973
|
+
} catch (error) {
|
|
6974
|
+
if (!isGuardConflict(error)) throw error;
|
|
6975
|
+
state = await loadNetworkPersonRegistry(db);
|
|
6976
|
+
const existing = state.aliases.find((item) => item.id === aliasId);
|
|
6977
|
+
if (existing) return { status: existing.status, alias: existing, replayed: true };
|
|
6978
|
+
const raced = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
6979
|
+
if (!raced.length) throw error;
|
|
6980
|
+
return ambiguousAlias(db, input, aliasId, claims, raced, `${mutationId}:review`, actor, now);
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date.now()) {
|
|
6984
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
6985
|
+
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
6986
|
+
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
6987
|
+
if (alias.revision !== input.expectedRevision || alias.status !== "ambiguous") throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
6988
|
+
if (input.decision === "exclude") {
|
|
6989
|
+
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
6990
|
+
await db.transact([
|
|
6991
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
6992
|
+
eventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
6993
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
6994
|
+
return next2;
|
|
6995
|
+
}
|
|
6996
|
+
const canonicalId = input.personId ? canonicalNetworkPersonId(input.personId, state.persons) : null;
|
|
6997
|
+
if (!canonicalId || canonicalId !== input.personId) throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
6998
|
+
const conflicting = alias.identityClaims.find((claim) => state.claims.some((row) => row.id === claim.id && row.personId !== canonicalId));
|
|
6999
|
+
if (conflicting) throw new NetworkPersonRegistryError("identity_conflict", "verified identity belongs to another Person; merge or choose that Person explicitly");
|
|
7000
|
+
const missing = alias.identityClaims.filter((claim) => !state.claims.some((row) => row.id === claim.id));
|
|
7001
|
+
const { candidatePersonIds: _candidatePersonIds, ...aliasWithoutCandidates } = alias;
|
|
7002
|
+
const next = { ...aliasWithoutCandidates, status: "linked", personId: canonicalId, revision: alias.revision + 1, updatedAt: now };
|
|
7003
|
+
await db.transact([
|
|
7004
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next } },
|
|
7005
|
+
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7006
|
+
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7007
|
+
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7008
|
+
eventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7009
|
+
], { mutationId, guards: [
|
|
7010
|
+
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7011
|
+
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
7012
|
+
] });
|
|
7013
|
+
return next;
|
|
7014
|
+
}
|
|
7015
|
+
async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now()) {
|
|
7016
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7017
|
+
const source = state.persons.find((person) => person.id === input.sourcePersonId);
|
|
7018
|
+
const target = state.persons.find((person) => person.id === input.targetPersonId);
|
|
7019
|
+
if (!source || !target || source.id === target.id || source.state !== "active" || target.state !== "active") throw new NetworkPersonRegistryError("invalid_merge", "merge requires two distinct active Persons");
|
|
7020
|
+
if (source.revision !== input.expectedRevision || target.revision !== input.expectedTargetRevision || canonicalNetworkPersonId(target.id, state.persons) !== target.id) {
|
|
7021
|
+
throw new NetworkPersonRegistryError("stale_merge", "Person changed; reload before merging");
|
|
7022
|
+
}
|
|
7023
|
+
const sourceNext = { ...source, state: "merged", mergedIntoId: target.id, revision: source.revision + 1, updatedAt: now };
|
|
7024
|
+
const targetNext = {
|
|
7025
|
+
...target,
|
|
7026
|
+
contactPolicy: {
|
|
7027
|
+
marketingSuppressed: source.contactPolicy.marketingSuppressed || target.contactPolicy.marketingSuppressed,
|
|
7028
|
+
discretionarySuppressed: source.contactPolicy.discretionarySuppressed || target.contactPolicy.discretionarySuppressed,
|
|
7029
|
+
...target.contactPolicy.reason || source.contactPolicy.reason ? { reason: target.contactPolicy.reason ?? source.contactPolicy.reason } : {},
|
|
7030
|
+
updatedAt: now
|
|
7031
|
+
},
|
|
7032
|
+
revision: target.revision + 1,
|
|
7033
|
+
updatedAt: now
|
|
7034
|
+
};
|
|
7035
|
+
await db.transact([
|
|
7036
|
+
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7037
|
+
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7038
|
+
eventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7039
|
+
], { mutationId, guards: [
|
|
7040
|
+
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7041
|
+
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
7042
|
+
] });
|
|
7043
|
+
return { source: sourceNext, target: targetNext };
|
|
7044
|
+
}
|
|
7045
|
+
async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7046
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7047
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7048
|
+
if (!person || person.state !== "merged" || person.revision !== input.expectedRevision) throw new NetworkPersonRegistryError("stale_unmerge", "merged Person changed; reload before unmerging");
|
|
7049
|
+
const next = { ...person, state: "active", revision: person.revision + 1, updatedAt: now };
|
|
7050
|
+
delete next.mergedIntoId;
|
|
7051
|
+
await db.transact([
|
|
7052
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7053
|
+
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7054
|
+
eventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7055
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7056
|
+
return next;
|
|
7057
|
+
}
|
|
7058
|
+
async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7059
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7060
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7061
|
+
if (!person || person.state !== "active" || canonicalNetworkPersonId(person.id, state.persons) !== person.id || person.revision !== input.expectedRevision) {
|
|
7062
|
+
throw new NetworkPersonRegistryError("stale_suppression", "active canonical Person changed; reload before updating suppression");
|
|
7063
|
+
}
|
|
7064
|
+
const policy = { marketingSuppressed: input.marketingSuppressed, discretionarySuppressed: input.discretionarySuppressed, ...input.reason ? { reason: input.reason } : {}, updatedAt: now };
|
|
7065
|
+
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7066
|
+
await db.transact([
|
|
7067
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7068
|
+
eventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7069
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7070
|
+
return next;
|
|
7071
|
+
}
|
|
7072
|
+
|
|
7073
|
+
// src/network-person-reveal.ts
|
|
7074
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7075
|
+
const reason = input.reason.trim();
|
|
7076
|
+
if (!reason || reason.length > 500) {
|
|
7077
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7078
|
+
}
|
|
7079
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7080
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7081
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7082
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7083
|
+
const event = {
|
|
7084
|
+
t: "update",
|
|
7085
|
+
ns: "networkPersonEvents",
|
|
7086
|
+
id: `network_person_event:${mutationId}`,
|
|
7087
|
+
attrs: {
|
|
7088
|
+
id: `network_person_event:${mutationId}`,
|
|
7089
|
+
kind: "pii_revealed",
|
|
7090
|
+
actorId: actor.id,
|
|
7091
|
+
...ids,
|
|
7092
|
+
mutationId,
|
|
7093
|
+
detail: { targetKind: input.kind, reason },
|
|
7094
|
+
createdAt: now
|
|
7095
|
+
}
|
|
7096
|
+
};
|
|
7097
|
+
await db.transact([event], {
|
|
7098
|
+
mutationId,
|
|
7099
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7100
|
+
});
|
|
7101
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7102
|
+
}
|
|
7103
|
+
|
|
7104
|
+
// src/worker-routes-network-person-registry.ts
|
|
7105
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7106
|
+
var MAX_BODY2 = 32e3;
|
|
7107
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7108
|
+
function redactedName(displayName) {
|
|
7109
|
+
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7110
|
+
}
|
|
7111
|
+
var redactedLink = (link) => ({
|
|
7112
|
+
id: link.id,
|
|
7113
|
+
personId: link.personId,
|
|
7114
|
+
chapterId: link.chapterId,
|
|
7115
|
+
state: link.state,
|
|
7116
|
+
createdAt: link.createdAt
|
|
7117
|
+
});
|
|
7118
|
+
var safePolicy = (policy) => ({
|
|
7119
|
+
marketingSuppressed: policy.marketingSuppressed,
|
|
7120
|
+
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7121
|
+
hasReason: Boolean(policy.reason),
|
|
7122
|
+
updatedAt: policy.updatedAt
|
|
7123
|
+
});
|
|
7124
|
+
var safeAlias = (alias) => ({
|
|
7125
|
+
id: alias.id,
|
|
7126
|
+
sourceSystem: alias.sourceSystem,
|
|
7127
|
+
displayNameHint: redactedName(alias.displayName),
|
|
7128
|
+
status: alias.status,
|
|
7129
|
+
personId: alias.personId ?? null,
|
|
7130
|
+
candidatePersonIds: alias.candidatePersonIds ?? [],
|
|
7131
|
+
consent: {
|
|
7132
|
+
marketing: alias.consent.marketing,
|
|
7133
|
+
discretionary: alias.consent.discretionary,
|
|
7134
|
+
recordedAt: alias.consent.recordedAt,
|
|
7135
|
+
hasBasis: Boolean(alias.consent.basis)
|
|
7136
|
+
},
|
|
7137
|
+
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7138
|
+
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7139
|
+
revision: alias.revision,
|
|
7140
|
+
updatedAt: alias.updatedAt
|
|
7141
|
+
});
|
|
7142
|
+
var safePerson = (person) => ({
|
|
7143
|
+
id: person.id,
|
|
7144
|
+
displayNameHint: redactedName(person.displayName),
|
|
7145
|
+
state: person.state,
|
|
7146
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7147
|
+
revision: person.revision,
|
|
7148
|
+
contactPolicy: safePolicy(person.contactPolicy)
|
|
7149
|
+
});
|
|
7150
|
+
var positiveRevision = (value) => {
|
|
7151
|
+
const parsed = Number(value);
|
|
7152
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
7153
|
+
};
|
|
7154
|
+
async function gate10(req, env, ctx) {
|
|
7155
|
+
const db = ctx.makeDb(env);
|
|
7156
|
+
const user = await ctx.verifyUser(req, env);
|
|
7157
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7158
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7159
|
+
return { db, user };
|
|
7160
|
+
}
|
|
7161
|
+
function registryError(error) {
|
|
7162
|
+
if (error instanceof NetworkPersonRegistryError) {
|
|
7163
|
+
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7164
|
+
return json({ error: error.message, code: error.code }, status);
|
|
7165
|
+
}
|
|
7166
|
+
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
7167
|
+
return json({ error: "registry changed; reload and retry", code: "stale_write" }, 409);
|
|
7168
|
+
}
|
|
7169
|
+
return null;
|
|
7170
|
+
}
|
|
7171
|
+
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
7172
|
+
if (url.pathname !== PATH2) return null;
|
|
7173
|
+
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
7174
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7175
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
7176
|
+
return json({ error: "request is too large" }, 413);
|
|
7177
|
+
}
|
|
7178
|
+
const got = await gate10(req, env, ctx);
|
|
7179
|
+
if ("response" in got) return got.response;
|
|
7180
|
+
const db = got.db;
|
|
7181
|
+
if (req.method === "POST") {
|
|
7182
|
+
let body;
|
|
7183
|
+
try {
|
|
7184
|
+
const text4 = await req.text();
|
|
7185
|
+
if (new TextEncoder().encode(text4).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7186
|
+
const parsed = JSON.parse(text4);
|
|
7187
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7188
|
+
body = parsed;
|
|
7189
|
+
} catch {
|
|
7190
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7191
|
+
}
|
|
7192
|
+
const command = typeof body.command === "string" ? body.command : "";
|
|
7193
|
+
const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
|
|
7194
|
+
const input = body.input && typeof body.input === "object" && !Array.isArray(body.input) ? body.input : {};
|
|
7195
|
+
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7196
|
+
const actionActor = { id: got.user.userId };
|
|
7197
|
+
try {
|
|
7198
|
+
if (command === "import") {
|
|
7199
|
+
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7200
|
+
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|
|
7201
|
+
const result = await importNetworkPerson(db, validated.value, mutationId, actionActor);
|
|
7202
|
+
return json({
|
|
7203
|
+
result: {
|
|
7204
|
+
status: result.status,
|
|
7205
|
+
alias: safeAlias(result.alias),
|
|
7206
|
+
..."person" in result && result.person ? { person: safePerson(result.person) } : {},
|
|
7207
|
+
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7208
|
+
}
|
|
7209
|
+
}, 201);
|
|
7210
|
+
}
|
|
7211
|
+
if (command === "review") {
|
|
7212
|
+
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or exclude" }, 400);
|
|
7213
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7214
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7215
|
+
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
7216
|
+
const result = await reviewNetworkPersonAlias(db, {
|
|
7217
|
+
aliasId: typeof input.aliasId === "string" ? input.aliasId : "",
|
|
7218
|
+
expectedRevision,
|
|
7219
|
+
decision: input.decision,
|
|
7220
|
+
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7221
|
+
}, mutationId, actionActor);
|
|
7222
|
+
return json({ result: safeAlias(result) });
|
|
7223
|
+
}
|
|
7224
|
+
if (command === "merge") {
|
|
7225
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7226
|
+
const expectedTargetRevision = positiveRevision(input.expectedTargetRevision);
|
|
7227
|
+
if (!expectedRevision || !expectedTargetRevision) return json({ error: "source and target revisions are required" }, 400);
|
|
7228
|
+
const result = await mergeNetworkPersons(db, {
|
|
7229
|
+
sourcePersonId: typeof input.sourcePersonId === "string" ? input.sourcePersonId : "",
|
|
7230
|
+
targetPersonId: typeof input.targetPersonId === "string" ? input.targetPersonId : "",
|
|
7231
|
+
expectedRevision,
|
|
7232
|
+
expectedTargetRevision
|
|
7233
|
+
}, mutationId, actionActor);
|
|
7234
|
+
return json({ result: { source: safePerson(result.source), target: safePerson(result.target) } });
|
|
7235
|
+
}
|
|
7236
|
+
if (command === "unmerge") {
|
|
7237
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7238
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7239
|
+
const result = await unmergeNetworkPerson(db, {
|
|
7240
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7241
|
+
expectedRevision
|
|
7242
|
+
}, mutationId, actionActor);
|
|
7243
|
+
return json({ result: safePerson(result) });
|
|
7244
|
+
}
|
|
7245
|
+
if (command === "suppression") {
|
|
7246
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7247
|
+
if (!expectedRevision || typeof input.marketingSuppressed !== "boolean" || typeof input.discretionarySuppressed !== "boolean") {
|
|
7248
|
+
return json({ error: "expectedRevision and both suppression flags are required" }, 400);
|
|
7249
|
+
}
|
|
7250
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7251
|
+
if (reason.length > 500) return json({ error: "suppression reason is too long" }, 400);
|
|
7252
|
+
const result = await suppressNetworkPerson(db, {
|
|
7253
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7254
|
+
expectedRevision,
|
|
7255
|
+
marketingSuppressed: input.marketingSuppressed,
|
|
7256
|
+
discretionarySuppressed: input.discretionarySuppressed,
|
|
7257
|
+
...reason ? { reason } : {}
|
|
7258
|
+
}, mutationId, actionActor);
|
|
7259
|
+
return json({ result: safePerson(result) });
|
|
7260
|
+
}
|
|
7261
|
+
if (command === "reveal") {
|
|
7262
|
+
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
7263
|
+
const result = await revealNetworkPersonRegistryRecord(db, {
|
|
7264
|
+
kind: input.kind,
|
|
7265
|
+
id: typeof input.id === "string" ? input.id : "",
|
|
7266
|
+
reason: typeof input.reason === "string" ? input.reason : ""
|
|
7267
|
+
}, mutationId, actionActor);
|
|
7268
|
+
return json({ sensitive: true, result });
|
|
7269
|
+
}
|
|
7270
|
+
return json({ error: "unknown registry command" }, 400);
|
|
7271
|
+
} catch (error) {
|
|
7272
|
+
const response = registryError(error);
|
|
7273
|
+
if (response) return response;
|
|
7274
|
+
throw error;
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7278
|
+
const persons = state.persons.map((person) => {
|
|
7279
|
+
const links = state.links.filter((link) => link.personId === person.id);
|
|
7280
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7281
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7282
|
+
return {
|
|
7283
|
+
id: person.id,
|
|
7284
|
+
displayNameHint: redactedName(person.displayName),
|
|
7285
|
+
state: person.state,
|
|
7286
|
+
canonicalPersonId: canonicalId,
|
|
7287
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7288
|
+
revision: person.revision,
|
|
7289
|
+
contactPolicy: safePolicy(person.contactPolicy),
|
|
7290
|
+
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7291
|
+
chapterLinks: links.map(redactedLink),
|
|
7292
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, links.map(redactedLink)).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7293
|
+
};
|
|
7294
|
+
});
|
|
7295
|
+
const aliases = state.aliases.map(safeAlias);
|
|
7296
|
+
return json({
|
|
7297
|
+
version: 1,
|
|
7298
|
+
mode: "shadow",
|
|
7299
|
+
writable: true,
|
|
7300
|
+
authoritativeForChapterOperation: false,
|
|
7301
|
+
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7302
|
+
persons,
|
|
7303
|
+
aliases,
|
|
7304
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7305
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7306
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7307
|
+
id: event.id,
|
|
7308
|
+
kind: event.kind,
|
|
7309
|
+
personId: event.personId ?? null,
|
|
7310
|
+
aliasId: event.aliasId ?? null,
|
|
7311
|
+
actorId: event.actorId,
|
|
7312
|
+
mutationId: event.mutationId,
|
|
7313
|
+
createdAt: event.createdAt
|
|
7314
|
+
}))
|
|
7315
|
+
});
|
|
7316
|
+
};
|
|
7317
|
+
|
|
6728
7318
|
// src/worker-routes-formation.ts
|
|
6729
7319
|
import { createRecord as createRecord3 } from "@odla-ai/crm";
|
|
6730
7320
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
@@ -7039,17 +7629,17 @@ async function builderLibraryDigest(document) {
|
|
|
7039
7629
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7040
7630
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7041
7631
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
7042
|
-
const
|
|
7043
|
-
return Array.isArray(
|
|
7632
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
7633
|
+
return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
|
|
7044
7634
|
}
|
|
7045
7635
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
7046
|
-
const
|
|
7047
|
-
return control2(Array.isArray(
|
|
7636
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
7637
|
+
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7048
7638
|
}
|
|
7049
7639
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
7050
7640
|
const id = `builder-library-local:${environment}`;
|
|
7051
|
-
const
|
|
7052
|
-
return local2(Array.isArray(
|
|
7641
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
7642
|
+
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
7053
7643
|
}
|
|
7054
7644
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
7055
7645
|
const { document, digest } = envelope;
|
|
@@ -7108,9 +7698,10 @@ async function verifySource(req, db, ctx) {
|
|
|
7108
7698
|
return null;
|
|
7109
7699
|
}
|
|
7110
7700
|
var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
7111
|
-
if (
|
|
7701
|
+
if (url.pathname !== BUILDER_LIBRARY_CONTROL_PATH) return null;
|
|
7112
7702
|
const settings = ctx.chapter.builderLibrary;
|
|
7113
7703
|
if (!settings) return json({ error: "not found" }, 404);
|
|
7704
|
+
if (req.method !== "POST") return json({ error: "unauthorized" }, 401);
|
|
7114
7705
|
const db = ctx.makeDb(env);
|
|
7115
7706
|
const verified = await verifySource(req, db, ctx);
|
|
7116
7707
|
if (!verified) return json({ error: "unauthorized" }, 401);
|
|
@@ -7539,6 +8130,7 @@ var BUILTIN_ROUTES = [
|
|
|
7539
8130
|
handleAdminNetworkRecords,
|
|
7540
8131
|
handleAdminNetworkNotes,
|
|
7541
8132
|
handleAdminNetworkSharedNotes,
|
|
8133
|
+
handleAdminNetworkPersonRegistry,
|
|
7542
8134
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
7543
8135
|
// get first refusal through options.routes, then this terminates unknown API
|
|
7544
8136
|
// paths with an explicit machine-readable 404.
|