@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.cjs
CHANGED
|
@@ -371,8 +371,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
371
371
|
function byOrder(a, b) {
|
|
372
372
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
373
373
|
}
|
|
374
|
-
function resolveTiers(group,
|
|
375
|
-
const stored =
|
|
374
|
+
function resolveTiers(group, rows5 = []) {
|
|
375
|
+
const stored = rows5.filter((tier) => tier.active);
|
|
376
376
|
if (stored.length) return [...stored].sort(byOrder);
|
|
377
377
|
return [{
|
|
378
378
|
id: "standard",
|
|
@@ -418,15 +418,15 @@ function rowToTier(row, groupId) {
|
|
|
418
418
|
}
|
|
419
419
|
async function loadTiers(db, group) {
|
|
420
420
|
const groupId = String(group.id ?? "");
|
|
421
|
-
let
|
|
421
|
+
let rows5;
|
|
422
422
|
try {
|
|
423
|
-
|
|
423
|
+
rows5 = (await db.query({
|
|
424
424
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
425
425
|
})).tiers;
|
|
426
426
|
} catch {
|
|
427
|
-
|
|
427
|
+
rows5 = void 0;
|
|
428
428
|
}
|
|
429
|
-
const list = Array.isArray(
|
|
429
|
+
const list = Array.isArray(rows5) ? rows5.map((row) => rowToTier(row, groupId)) : [];
|
|
430
430
|
return resolveTiers(group, list);
|
|
431
431
|
}
|
|
432
432
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -1480,27 +1480,27 @@ async function hasSignupControl(db, targetId, environment, active) {
|
|
|
1480
1480
|
if (db.aggregate) {
|
|
1481
1481
|
return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
|
|
1482
1482
|
}
|
|
1483
|
-
const
|
|
1484
|
-
return Array.isArray(
|
|
1483
|
+
const rows5 = (await db.query({ signupControls: {} })).signupControls;
|
|
1484
|
+
return Array.isArray(rows5) && rows5.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
|
|
1485
1485
|
}
|
|
1486
1486
|
async function loadLatestSignupControl(db, targetId, environment) {
|
|
1487
1487
|
if (!await hasSignupControl(db, targetId, environment)) return null;
|
|
1488
|
-
const
|
|
1488
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1489
1489
|
where: { targetId, environment },
|
|
1490
1490
|
order: { revision: "desc" },
|
|
1491
1491
|
limit: 1
|
|
1492
1492
|
} } })).signupControls;
|
|
1493
|
-
const latest = Array.isArray(
|
|
1493
|
+
const latest = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1494
1494
|
return asStored(latest);
|
|
1495
1495
|
}
|
|
1496
1496
|
async function loadActiveSignupControl(db, targetId, environment) {
|
|
1497
1497
|
if (!await hasSignupControl(db, targetId, environment, true)) return null;
|
|
1498
|
-
const
|
|
1498
|
+
const rows5 = (await db.query({ signupControls: { $: {
|
|
1499
1499
|
where: { targetId, environment, active: true },
|
|
1500
1500
|
order: { revision: "desc" },
|
|
1501
1501
|
limit: 1
|
|
1502
1502
|
} } })).signupControls;
|
|
1503
|
-
const active = Array.isArray(
|
|
1503
|
+
const active = Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
|
|
1504
1504
|
return asStored(active);
|
|
1505
1505
|
}
|
|
1506
1506
|
function controlledApplication(base, document) {
|
|
@@ -2876,33 +2876,33 @@ var control = (row) => row && row.document && typeof row.document === "object" ?
|
|
|
2876
2876
|
var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
|
|
2877
2877
|
async function exists(db, ns, where) {
|
|
2878
2878
|
if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
|
|
2879
|
-
const
|
|
2880
|
-
return
|
|
2879
|
+
const rows5 = (await db.query({ [ns]: {} }))[ns] ?? [];
|
|
2880
|
+
return rows5.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
|
|
2881
2881
|
}
|
|
2882
2882
|
async function loadActiveMemberContentControl(db, targetId, environment) {
|
|
2883
2883
|
const where = { targetId, environment, active: true };
|
|
2884
2884
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2885
|
-
const
|
|
2885
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2886
2886
|
where,
|
|
2887
2887
|
order: { revision: "desc" },
|
|
2888
2888
|
limit: 1
|
|
2889
2889
|
} } })).memberContentControls;
|
|
2890
|
-
return control(Array.isArray(
|
|
2890
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2891
2891
|
}
|
|
2892
2892
|
async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
2893
2893
|
const where = { targetId, environment };
|
|
2894
2894
|
if (!await exists(db, "memberContentControls", where)) return null;
|
|
2895
|
-
const
|
|
2895
|
+
const rows5 = (await db.query({ memberContentControls: { $: {
|
|
2896
2896
|
where,
|
|
2897
2897
|
order: { revision: "desc" },
|
|
2898
2898
|
limit: 1
|
|
2899
2899
|
} } })).memberContentControls;
|
|
2900
|
-
return control(Array.isArray(
|
|
2900
|
+
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
2901
2901
|
}
|
|
2902
2902
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
2903
2903
|
const id = `member-content-local:${environment}`;
|
|
2904
|
-
const
|
|
2905
|
-
return local(Array.isArray(
|
|
2904
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
|
|
2905
|
+
return local(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
2906
2906
|
}
|
|
2907
2907
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
2908
2908
|
const { document, digest } = envelope;
|
|
@@ -2957,12 +2957,12 @@ async function groupAndTiers(db, chapterId) {
|
|
|
2957
2957
|
}
|
|
2958
2958
|
async function memberTierId(db, chapterId, email) {
|
|
2959
2959
|
if (!email) return null;
|
|
2960
|
-
let
|
|
2960
|
+
let rows5 = (await db.query({ applications: { $: {
|
|
2961
2961
|
where: { emailNormalized: normalizeAdmissionEmail(email) },
|
|
2962
2962
|
limit: 20
|
|
2963
2963
|
} } })).applications;
|
|
2964
|
-
if (!
|
|
2965
|
-
const latest = Array.isArray(
|
|
2964
|
+
if (!rows5?.length) rows5 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
2965
|
+
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;
|
|
2966
2966
|
return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
|
|
2967
2967
|
}
|
|
2968
2968
|
async function activeDocuments(db, chapterId, environment) {
|
|
@@ -3688,8 +3688,8 @@ function makeCalendar(env) {
|
|
|
3688
3688
|
}
|
|
3689
3689
|
async function firstRow(db, ns, q) {
|
|
3690
3690
|
const res = await db.query({ [ns]: { $: q } });
|
|
3691
|
-
const
|
|
3692
|
-
return Array.isArray(
|
|
3691
|
+
const rows5 = res[ns];
|
|
3692
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
3693
3693
|
}
|
|
3694
3694
|
async function computeSlots(cal, cfg) {
|
|
3695
3695
|
const { from, to } = slotWindow(Date.now(), cfg.windowDays);
|
|
@@ -4089,8 +4089,8 @@ function createStripeProvider(config) {
|
|
|
4089
4089
|
const sk = requireSecret(secretKey);
|
|
4090
4090
|
const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
|
|
4091
4091
|
if (!charges.ok) fail("charges list", charges);
|
|
4092
|
-
const
|
|
4093
|
-
const paid =
|
|
4092
|
+
const rows5 = charges.body.data ?? [];
|
|
4093
|
+
const paid = rows5.filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
4094
4094
|
const charge = paid[paid.length - 1];
|
|
4095
4095
|
if (!charge) {
|
|
4096
4096
|
const err = new Error("no paid charge to refund");
|
|
@@ -4198,8 +4198,8 @@ async function issueGiftClaim(db, intent, provider, now) {
|
|
|
4198
4198
|
throw new Error("gift issuance conflict");
|
|
4199
4199
|
}
|
|
4200
4200
|
}
|
|
4201
|
-
function buyerGiftClaims(
|
|
4202
|
-
return
|
|
4201
|
+
function buyerGiftClaims(rows5, now) {
|
|
4202
|
+
return rows5.map((row) => {
|
|
4203
4203
|
const stored = String(row.status ?? "pending");
|
|
4204
4204
|
const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
|
|
4205
4205
|
return {
|
|
@@ -4491,8 +4491,8 @@ function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
|
4491
4491
|
|
|
4492
4492
|
// src/stripe-webhook-partition.ts
|
|
4493
4493
|
var firstRow2 = async (db, ns, q) => {
|
|
4494
|
-
const
|
|
4495
|
-
return Array.isArray(
|
|
4494
|
+
const rows5 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4495
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4496
4496
|
};
|
|
4497
4497
|
var stripeEventDigest = async (rawBody) => {
|
|
4498
4498
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
@@ -4562,8 +4562,8 @@ async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
|
4562
4562
|
// src/worker-routes-payments.ts
|
|
4563
4563
|
async function firstRow3(db, ns, q) {
|
|
4564
4564
|
const res = await db.query({ [ns]: { $: q } });
|
|
4565
|
-
const
|
|
4566
|
-
return Array.isArray(
|
|
4565
|
+
const rows5 = res[ns];
|
|
4566
|
+
return Array.isArray(rows5) ? rows5[0] : void 0;
|
|
4567
4567
|
}
|
|
4568
4568
|
async function findApplication(db, event) {
|
|
4569
4569
|
if ("applicationId" in event && event.applicationId) {
|
|
@@ -5029,8 +5029,8 @@ async function upcomingEvents(env) {
|
|
|
5029
5029
|
const res = await cal.availability.upcoming();
|
|
5030
5030
|
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
5031
5031
|
}
|
|
5032
|
-
function toReconcile(
|
|
5033
|
-
return
|
|
5032
|
+
function toReconcile(rows5) {
|
|
5033
|
+
return rows5.map((m) => ({
|
|
5034
5034
|
id: String(m.id),
|
|
5035
5035
|
applicationId: String(m.applicationId),
|
|
5036
5036
|
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
@@ -5052,12 +5052,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5052
5052
|
const query = await db.query({
|
|
5053
5053
|
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
5054
5054
|
});
|
|
5055
|
-
let
|
|
5056
|
-
if (Number.isFinite(from))
|
|
5057
|
-
if (Number.isFinite(to))
|
|
5055
|
+
let rows5 = Array.isArray(query.meetings) ? query.meetings : [];
|
|
5056
|
+
if (Number.isFinite(from)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
5057
|
+
if (Number.isFinite(to)) rows5 = rows5.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
5058
5058
|
let decisions = [];
|
|
5059
5059
|
try {
|
|
5060
|
-
decisions = reconcileMeetings(toReconcile(
|
|
5060
|
+
decisions = reconcileMeetings(toReconcile(rows5), await upcomingEvents(env), Date.now());
|
|
5061
5061
|
} catch {
|
|
5062
5062
|
}
|
|
5063
5063
|
const patched = /* @__PURE__ */ new Map();
|
|
@@ -5084,7 +5084,7 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
5084
5084
|
};
|
|
5085
5085
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
5086
5086
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5087
|
-
const meetings =
|
|
5087
|
+
const meetings = rows5.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
5088
5088
|
return json({ meetings, adopted: decisions.length, timezone });
|
|
5089
5089
|
};
|
|
5090
5090
|
|
|
@@ -5105,16 +5105,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
5105
5105
|
});
|
|
5106
5106
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
5107
5107
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
5108
|
-
const
|
|
5109
|
-
if (
|
|
5110
|
-
const result = await backfillCrm(crmDeps2(
|
|
5108
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5109
|
+
if (gate11 instanceof Response) return gate11;
|
|
5110
|
+
const result = await backfillCrm(crmDeps2(gate11.db, ctx));
|
|
5111
5111
|
return json({ ok: true, ...result });
|
|
5112
5112
|
};
|
|
5113
5113
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
5114
5114
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
5115
|
-
const
|
|
5116
|
-
if (
|
|
5117
|
-
const { db } =
|
|
5115
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5116
|
+
if (gate11 instanceof Response) return gate11;
|
|
5117
|
+
const { db } = gate11;
|
|
5118
5118
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
5119
5119
|
const [data, roleList] = await Promise.all([
|
|
5120
5120
|
db.query({
|
|
@@ -5160,16 +5160,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
5160
5160
|
});
|
|
5161
5161
|
}
|
|
5162
5162
|
}
|
|
5163
|
-
const
|
|
5163
|
+
const rows5 = [...people.values()].sort(
|
|
5164
5164
|
(x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
|
|
5165
5165
|
);
|
|
5166
|
-
return json({ people:
|
|
5166
|
+
return json({ people: rows5 });
|
|
5167
5167
|
};
|
|
5168
5168
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
5169
5169
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
5170
|
-
const
|
|
5171
|
-
if (
|
|
5172
|
-
const { db } =
|
|
5170
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5171
|
+
if (gate11 instanceof Response) return gate11;
|
|
5172
|
+
const { db } = gate11;
|
|
5173
5173
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
5174
5174
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
5175
5175
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -5180,9 +5180,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
5180
5180
|
};
|
|
5181
5181
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
5182
5182
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
5183
|
-
const
|
|
5184
|
-
if (
|
|
5185
|
-
const { db, actor } =
|
|
5183
|
+
const gate11 = await adminGate(req, env, ctx);
|
|
5184
|
+
if (gate11 instanceof Response) return gate11;
|
|
5185
|
+
const { db, actor } = gate11;
|
|
5186
5186
|
let body;
|
|
5187
5187
|
try {
|
|
5188
5188
|
body = await req.json();
|
|
@@ -6751,6 +6751,596 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6751
6751
|
}
|
|
6752
6752
|
};
|
|
6753
6753
|
|
|
6754
|
+
// src/network-person-registry.ts
|
|
6755
|
+
var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
6756
|
+
var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
6757
|
+
function normalizeNetworkIdentity(kind, value) {
|
|
6758
|
+
const trimmed = value.trim();
|
|
6759
|
+
if (kind === "email") return trimmed.toLowerCase();
|
|
6760
|
+
if (kind === "phone") return trimmed.replace(/[\s().-]/g, "");
|
|
6761
|
+
return trimmed;
|
|
6762
|
+
}
|
|
6763
|
+
function emailLooksReal(value) {
|
|
6764
|
+
const at = value.lastIndexOf("@");
|
|
6765
|
+
if (at <= 0 || at === value.length - 1) return false;
|
|
6766
|
+
const domain = value.slice(at + 1).toLowerCase();
|
|
6767
|
+
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
6768
|
+
}
|
|
6769
|
+
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
6770
|
+
const errors = [];
|
|
6771
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, errors: ["import must be an object"] };
|
|
6772
|
+
const raw = input;
|
|
6773
|
+
if (raw.sourceKind !== "external_reimport") errors.push("sourceKind must be external_reimport");
|
|
6774
|
+
if (raw.approved !== true || raw.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
6775
|
+
const sourceSystem = typeof raw.sourceSystem === "string" ? raw.sourceSystem.trim().toLowerCase() : "";
|
|
6776
|
+
const sourceRecordId = typeof raw.sourceRecordId === "string" ? raw.sourceRecordId.trim() : "";
|
|
6777
|
+
const provenance = typeof raw.provenance === "string" ? raw.provenance.trim() : "";
|
|
6778
|
+
const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
|
|
6779
|
+
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
6780
|
+
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
6781
|
+
if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
|
|
6782
|
+
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
6783
|
+
const rawConsent = raw.consent && typeof raw.consent === "object" && !Array.isArray(raw.consent) ? raw.consent : {};
|
|
6784
|
+
const consentStates = ["unknown", "granted", "denied"];
|
|
6785
|
+
const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
|
|
6786
|
+
const discretionary = typeof rawConsent.discretionary === "string" ? rawConsent.discretionary : "";
|
|
6787
|
+
const recordedAt = Number(rawConsent.recordedAt);
|
|
6788
|
+
const basis = typeof rawConsent.basis === "string" ? rawConsent.basis.trim() : "";
|
|
6789
|
+
if (!consentStates.includes(marketing) || !consentStates.includes(discretionary) || !Number.isSafeInteger(recordedAt) || recordedAt <= 0 || !basis || basis.length > 500) {
|
|
6790
|
+
errors.push("consent must include marketing and discretionary states, recordedAt, and a bounded basis");
|
|
6791
|
+
}
|
|
6792
|
+
const identities = [];
|
|
6793
|
+
if (!Array.isArray(raw.identities) || raw.identities.length < 1 || raw.identities.length > 8) {
|
|
6794
|
+
errors.push("one to eight verified identities are required");
|
|
6795
|
+
} else {
|
|
6796
|
+
for (const item of raw.identities) {
|
|
6797
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6798
|
+
errors.push("identity must be an object");
|
|
6799
|
+
continue;
|
|
6800
|
+
}
|
|
6801
|
+
const candidate = item;
|
|
6802
|
+
const kind = candidate.kind;
|
|
6803
|
+
const value = typeof candidate.value === "string" ? candidate.value : "";
|
|
6804
|
+
const verifiedAt = Number(candidate.verifiedAt);
|
|
6805
|
+
if (kind !== "email" && kind !== "phone" && kind !== "external") {
|
|
6806
|
+
errors.push("identity kind is invalid");
|
|
6807
|
+
continue;
|
|
6808
|
+
}
|
|
6809
|
+
const normalized = normalizeNetworkIdentity(kind, value);
|
|
6810
|
+
if (!normalized || normalized.length > 320 || !Number.isSafeInteger(verifiedAt) || verifiedAt <= 0) {
|
|
6811
|
+
errors.push("identity value and verifiedAt are required and bounded");
|
|
6812
|
+
continue;
|
|
6813
|
+
}
|
|
6814
|
+
if (kind === "email" && !emailLooksReal(normalized)) {
|
|
6815
|
+
errors.push("development or invalid email identities cannot become Persons");
|
|
6816
|
+
continue;
|
|
6817
|
+
}
|
|
6818
|
+
identities.push({ kind, value: normalized, verifiedAt });
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6821
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6822
|
+
for (const identity of identities) {
|
|
6823
|
+
const key = `${identity.kind}:${identity.value}`;
|
|
6824
|
+
if (seen.has(key)) errors.push("duplicate identity in import");
|
|
6825
|
+
seen.add(key);
|
|
6826
|
+
}
|
|
6827
|
+
const chapterLinks = [];
|
|
6828
|
+
if (raw.chapterLinks !== void 0) {
|
|
6829
|
+
if (!Array.isArray(raw.chapterLinks) || raw.chapterLinks.length > allowedChapterIds.length) errors.push("chapterLinks are invalid or too numerous");
|
|
6830
|
+
else for (const item of raw.chapterLinks) {
|
|
6831
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6832
|
+
errors.push("chapter link must be an object");
|
|
6833
|
+
continue;
|
|
6834
|
+
}
|
|
6835
|
+
const chapterId = typeof item.chapterId === "string" ? String(item.chapterId) : "";
|
|
6836
|
+
const chapterPersonId = typeof item.chapterPersonId === "string" ? String(item.chapterPersonId).trim() : "";
|
|
6837
|
+
if (!CHAPTER_ID.test(chapterId) || !allowedChapterIds.includes(chapterId) || !chapterPersonId || chapterPersonId.length > 200) errors.push("chapter link is outside the configured network");
|
|
6838
|
+
else chapterLinks.push({ chapterId, chapterPersonId });
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
if (errors.length) return { ok: false, errors };
|
|
6842
|
+
return { ok: true, errors: [], value: {
|
|
6843
|
+
sourceKind: "external_reimport",
|
|
6844
|
+
sourceSystem,
|
|
6845
|
+
sourceRecordId,
|
|
6846
|
+
provenance,
|
|
6847
|
+
approved: true,
|
|
6848
|
+
isRealPerson: true,
|
|
6849
|
+
displayName,
|
|
6850
|
+
consent: {
|
|
6851
|
+
marketing,
|
|
6852
|
+
discretionary,
|
|
6853
|
+
recordedAt,
|
|
6854
|
+
basis
|
|
6855
|
+
},
|
|
6856
|
+
identities,
|
|
6857
|
+
...chapterLinks.length ? { chapterLinks } : {}
|
|
6858
|
+
} };
|
|
6859
|
+
}
|
|
6860
|
+
async function sha256(value) {
|
|
6861
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
6862
|
+
return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
6863
|
+
}
|
|
6864
|
+
async function networkIdentityClaim(input) {
|
|
6865
|
+
const normalized = normalizeNetworkIdentity(input.kind, input.value);
|
|
6866
|
+
const digest = await sha256(`${input.kind}\0${normalized}`);
|
|
6867
|
+
const hint = input.kind === "email" ? `${normalized.slice(0, 1)}***@${normalized.split("@")[1]}` : input.kind === "phone" ? `***${normalized.slice(-4)}` : void 0;
|
|
6868
|
+
return { id: `identity_${digest}`, kind: input.kind, value: normalized, ...hint ? { hint } : {}, verifiedAt: input.verifiedAt };
|
|
6869
|
+
}
|
|
6870
|
+
async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
6871
|
+
return `alias_${await sha256(`${sourceSystem}\0${sourceRecordId}`)}`;
|
|
6872
|
+
}
|
|
6873
|
+
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6874
|
+
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6875
|
+
}
|
|
6876
|
+
function projectNetworkContactPolicy(policy, links) {
|
|
6877
|
+
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6878
|
+
}
|
|
6879
|
+
function canonicalNetworkPersonId(personId, persons) {
|
|
6880
|
+
const byId = new Map(persons.map((person) => [person.id, person]));
|
|
6881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6882
|
+
let current = personId;
|
|
6883
|
+
while (true) {
|
|
6884
|
+
if (seen.has(current)) return null;
|
|
6885
|
+
seen.add(current);
|
|
6886
|
+
const person = byId.get(current);
|
|
6887
|
+
if (!person) return null;
|
|
6888
|
+
if (!person.mergedIntoId) return person.id;
|
|
6889
|
+
current = person.mergedIntoId;
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
// src/network-person-store.ts
|
|
6894
|
+
var NetworkPersonRegistryError = class extends Error {
|
|
6895
|
+
constructor(code, message) {
|
|
6896
|
+
super(message);
|
|
6897
|
+
this.code = code;
|
|
6898
|
+
}
|
|
6899
|
+
code;
|
|
6900
|
+
};
|
|
6901
|
+
var rows4 = (value) => Array.isArray(value) ? value : [];
|
|
6902
|
+
async function loadNetworkPersonRegistry(db) {
|
|
6903
|
+
const result = await db.query({
|
|
6904
|
+
networkPersons: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6905
|
+
networkIdentityClaims: { $: { limit: 1e3 } },
|
|
6906
|
+
networkPersonAliases: { $: { order: { updatedAt: "desc" }, limit: 1e3 } },
|
|
6907
|
+
networkChapterPersonLinks: { $: { limit: 1e3 } },
|
|
6908
|
+
networkPersonEvents: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
6909
|
+
});
|
|
6910
|
+
return {
|
|
6911
|
+
persons: rows4(result.networkPersons),
|
|
6912
|
+
claims: rows4(result.networkIdentityClaims),
|
|
6913
|
+
aliases: rows4(result.networkPersonAliases),
|
|
6914
|
+
links: rows4(result.networkChapterPersonLinks),
|
|
6915
|
+
events: rows4(result.networkPersonEvents)
|
|
6916
|
+
};
|
|
6917
|
+
}
|
|
6918
|
+
var eventOp = (kind, mutationId, actor, now, detail, ids = {}) => ({
|
|
6919
|
+
t: "update",
|
|
6920
|
+
ns: "networkPersonEvents",
|
|
6921
|
+
id: `network_person_event:${mutationId}`,
|
|
6922
|
+
attrs: { id: `network_person_event:${mutationId}`, kind, actorId: actor.id, ...ids, mutationId, detail, createdAt: now }
|
|
6923
|
+
});
|
|
6924
|
+
var defaultPolicy = (now, consent) => ({
|
|
6925
|
+
marketingSuppressed: consent.marketing !== "granted",
|
|
6926
|
+
discretionarySuppressed: consent.discretionary !== "granted",
|
|
6927
|
+
updatedAt: now
|
|
6928
|
+
});
|
|
6929
|
+
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
6930
|
+
async function linkOps(personId, links, now) {
|
|
6931
|
+
return Promise.all(links.map(async (link) => {
|
|
6932
|
+
const id = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
6933
|
+
return { t: "update", ns: "networkChapterPersonLinks", id, attrs: { id, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, state: "shadow", createdAt: now } };
|
|
6934
|
+
}));
|
|
6935
|
+
}
|
|
6936
|
+
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
6937
|
+
const alias = {
|
|
6938
|
+
id: aliasId,
|
|
6939
|
+
sourceSystem: input.sourceSystem,
|
|
6940
|
+
sourceRecordId: input.sourceRecordId,
|
|
6941
|
+
sourceKey: aliasId,
|
|
6942
|
+
displayName: input.displayName,
|
|
6943
|
+
provenance: input.provenance,
|
|
6944
|
+
consent: input.consent,
|
|
6945
|
+
status: "ambiguous",
|
|
6946
|
+
candidatePersonIds: candidates,
|
|
6947
|
+
identityClaims: claims,
|
|
6948
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6949
|
+
revision: 1,
|
|
6950
|
+
createdAt: now,
|
|
6951
|
+
updatedAt: now
|
|
6952
|
+
};
|
|
6953
|
+
await db.transact([
|
|
6954
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...alias } },
|
|
6955
|
+
eventOp("import_queued_ambiguous", mutationId, actor, now, { candidateCount: candidates.length, sourceSystem: input.sourceSystem }, { aliasId })
|
|
6956
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: false }] });
|
|
6957
|
+
return { status: "ambiguous", alias };
|
|
6958
|
+
}
|
|
6959
|
+
async function importNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
6960
|
+
const aliasId = await networkSourceKey(input.sourceSystem, input.sourceRecordId);
|
|
6961
|
+
let state = await loadNetworkPersonRegistry(db);
|
|
6962
|
+
const replay = state.aliases.find((alias2) => alias2.id === aliasId);
|
|
6963
|
+
if (replay) return { status: replay.status, alias: replay, replayed: true };
|
|
6964
|
+
const claims = await Promise.all(input.identities.map(networkIdentityClaim));
|
|
6965
|
+
const candidates = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
6966
|
+
if (candidates.length) return ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now);
|
|
6967
|
+
const personId = `person_${aliasId.slice("alias_".length)}`;
|
|
6968
|
+
const person = { id: personId, displayName: input.displayName, state: "active", contactPolicy: defaultPolicy(now, input.consent), revision: 1, createdAt: now, updatedAt: now };
|
|
6969
|
+
const alias = {
|
|
6970
|
+
id: aliasId,
|
|
6971
|
+
sourceSystem: input.sourceSystem,
|
|
6972
|
+
sourceRecordId: input.sourceRecordId,
|
|
6973
|
+
sourceKey: aliasId,
|
|
6974
|
+
displayName: input.displayName,
|
|
6975
|
+
provenance: input.provenance,
|
|
6976
|
+
consent: input.consent,
|
|
6977
|
+
status: "linked",
|
|
6978
|
+
personId,
|
|
6979
|
+
identityClaims: claims,
|
|
6980
|
+
chapterLinks: [...input.chapterLinks ?? []],
|
|
6981
|
+
revision: 1,
|
|
6982
|
+
createdAt: now,
|
|
6983
|
+
updatedAt: now
|
|
6984
|
+
};
|
|
6985
|
+
const claimOps = claims.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId, createdAt: now } }));
|
|
6986
|
+
const ops = [
|
|
6987
|
+
{ t: "update", ns: "networkPersons", id: personId, attrs: { ...person } },
|
|
6988
|
+
{ t: "update", ns: "networkPersonAliases", id: aliasId, attrs: { ...alias } },
|
|
6989
|
+
...claimOps,
|
|
6990
|
+
...await linkOps(personId, input.chapterLinks ?? [], now),
|
|
6991
|
+
eventOp("person_imported", mutationId, actor, now, { sourceSystem: input.sourceSystem, identityCount: claims.length }, { personId, aliasId })
|
|
6992
|
+
];
|
|
6993
|
+
try {
|
|
6994
|
+
await db.transact(ops, { mutationId, guards: [
|
|
6995
|
+
{ ns: "networkPersonAliases", id: aliasId, exists: false },
|
|
6996
|
+
...claims.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
6997
|
+
] });
|
|
6998
|
+
return { status: "linked", alias, person };
|
|
6999
|
+
} catch (error) {
|
|
7000
|
+
if (!isGuardConflict(error)) throw error;
|
|
7001
|
+
state = await loadNetworkPersonRegistry(db);
|
|
7002
|
+
const existing = state.aliases.find((item) => item.id === aliasId);
|
|
7003
|
+
if (existing) return { status: existing.status, alias: existing, replayed: true };
|
|
7004
|
+
const raced = [...new Set(claims.flatMap((claim) => state.claims.filter((row) => row.id === claim.id).map((row) => row.personId)))];
|
|
7005
|
+
if (!raced.length) throw error;
|
|
7006
|
+
return ambiguousAlias(db, input, aliasId, claims, raced, `${mutationId}:review`, actor, now);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
async function reviewNetworkPersonAlias(db, input, mutationId, actor, now = Date.now()) {
|
|
7010
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7011
|
+
const alias = state.aliases.find((row) => row.id === input.aliasId);
|
|
7012
|
+
if (!alias) throw new NetworkPersonRegistryError("alias_not_found", "source alias not found");
|
|
7013
|
+
if (alias.revision !== input.expectedRevision || alias.status !== "ambiguous") throw new NetworkPersonRegistryError("stale_review", "source alias changed; reload before reviewing");
|
|
7014
|
+
if (input.decision === "exclude") {
|
|
7015
|
+
const next2 = { ...alias, status: "excluded", revision: alias.revision + 1, updatedAt: now };
|
|
7016
|
+
await db.transact([
|
|
7017
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next2 } },
|
|
7018
|
+
eventOp("alias_excluded", mutationId, actor, now, { sourceSystem: alias.sourceSystem }, { aliasId: alias.id })
|
|
7019
|
+
], { mutationId, guards: [{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } }] });
|
|
7020
|
+
return next2;
|
|
7021
|
+
}
|
|
7022
|
+
const canonicalId = input.personId ? canonicalNetworkPersonId(input.personId, state.persons) : null;
|
|
7023
|
+
if (!canonicalId || canonicalId !== input.personId) throw new NetworkPersonRegistryError("person_not_active", "link target must be an active canonical Person");
|
|
7024
|
+
const conflicting = alias.identityClaims.find((claim) => state.claims.some((row) => row.id === claim.id && row.personId !== canonicalId));
|
|
7025
|
+
if (conflicting) throw new NetworkPersonRegistryError("identity_conflict", "verified identity belongs to another Person; merge or choose that Person explicitly");
|
|
7026
|
+
const missing = alias.identityClaims.filter((claim) => !state.claims.some((row) => row.id === claim.id));
|
|
7027
|
+
const { candidatePersonIds: _candidatePersonIds, ...aliasWithoutCandidates } = alias;
|
|
7028
|
+
const next = { ...aliasWithoutCandidates, status: "linked", personId: canonicalId, revision: alias.revision + 1, updatedAt: now };
|
|
7029
|
+
await db.transact([
|
|
7030
|
+
{ t: "update", ns: "networkPersonAliases", id: alias.id, attrs: { ...next } },
|
|
7031
|
+
{ t: "retract", ns: "networkPersonAliases", id: alias.id, attrs: ["candidatePersonIds"] },
|
|
7032
|
+
...missing.map((claim) => ({ t: "update", ns: "networkIdentityClaims", id: claim.id, attrs: { ...claim, personId: canonicalId, createdAt: now } })),
|
|
7033
|
+
...await linkOps(canonicalId, alias.chapterLinks, now),
|
|
7034
|
+
eventOp("alias_linked", mutationId, actor, now, { identityCount: alias.identityClaims.length }, { personId: canonicalId, aliasId: alias.id })
|
|
7035
|
+
], { mutationId, guards: [
|
|
7036
|
+
{ ns: "networkPersonAliases", id: alias.id, exists: true, equals: { revision: alias.revision, status: "ambiguous" } },
|
|
7037
|
+
...missing.map((claim) => ({ ns: "networkIdentityClaims", id: claim.id, exists: false }))
|
|
7038
|
+
] });
|
|
7039
|
+
return next;
|
|
7040
|
+
}
|
|
7041
|
+
async function mergeNetworkPersons(db, input, mutationId, actor, now = Date.now()) {
|
|
7042
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7043
|
+
const source = state.persons.find((person) => person.id === input.sourcePersonId);
|
|
7044
|
+
const target = state.persons.find((person) => person.id === input.targetPersonId);
|
|
7045
|
+
if (!source || !target || source.id === target.id || source.state !== "active" || target.state !== "active") throw new NetworkPersonRegistryError("invalid_merge", "merge requires two distinct active Persons");
|
|
7046
|
+
if (source.revision !== input.expectedRevision || target.revision !== input.expectedTargetRevision || canonicalNetworkPersonId(target.id, state.persons) !== target.id) {
|
|
7047
|
+
throw new NetworkPersonRegistryError("stale_merge", "Person changed; reload before merging");
|
|
7048
|
+
}
|
|
7049
|
+
const sourceNext = { ...source, state: "merged", mergedIntoId: target.id, revision: source.revision + 1, updatedAt: now };
|
|
7050
|
+
const targetNext = {
|
|
7051
|
+
...target,
|
|
7052
|
+
contactPolicy: {
|
|
7053
|
+
marketingSuppressed: source.contactPolicy.marketingSuppressed || target.contactPolicy.marketingSuppressed,
|
|
7054
|
+
discretionarySuppressed: source.contactPolicy.discretionarySuppressed || target.contactPolicy.discretionarySuppressed,
|
|
7055
|
+
...target.contactPolicy.reason || source.contactPolicy.reason ? { reason: target.contactPolicy.reason ?? source.contactPolicy.reason } : {},
|
|
7056
|
+
updatedAt: now
|
|
7057
|
+
},
|
|
7058
|
+
revision: target.revision + 1,
|
|
7059
|
+
updatedAt: now
|
|
7060
|
+
};
|
|
7061
|
+
await db.transact([
|
|
7062
|
+
{ t: "update", ns: "networkPersons", id: source.id, attrs: { ...sourceNext } },
|
|
7063
|
+
{ t: "update", ns: "networkPersons", id: target.id, attrs: { ...targetNext } },
|
|
7064
|
+
eventOp("persons_merged", mutationId, actor, now, { targetPersonId: target.id }, { personId: source.id })
|
|
7065
|
+
], { mutationId, guards: [
|
|
7066
|
+
{ ns: "networkPersons", id: source.id, exists: true, equals: { revision: source.revision, state: "active" } },
|
|
7067
|
+
{ ns: "networkPersons", id: target.id, exists: true, equals: { revision: target.revision, state: "active" } }
|
|
7068
|
+
] });
|
|
7069
|
+
return { source: sourceNext, target: targetNext };
|
|
7070
|
+
}
|
|
7071
|
+
async function unmergeNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7072
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7073
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7074
|
+
if (!person || person.state !== "merged" || person.revision !== input.expectedRevision) throw new NetworkPersonRegistryError("stale_unmerge", "merged Person changed; reload before unmerging");
|
|
7075
|
+
const next = { ...person, state: "active", revision: person.revision + 1, updatedAt: now };
|
|
7076
|
+
delete next.mergedIntoId;
|
|
7077
|
+
await db.transact([
|
|
7078
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { state: "active", revision: next.revision, updatedAt: now } },
|
|
7079
|
+
{ t: "retract", ns: "networkPersons", id: person.id, attrs: ["mergedIntoId"] },
|
|
7080
|
+
eventOp("person_unmerged", mutationId, actor, now, {}, { personId: person.id })
|
|
7081
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "merged", mergedIntoId: person.mergedIntoId } }] });
|
|
7082
|
+
return next;
|
|
7083
|
+
}
|
|
7084
|
+
async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.now()) {
|
|
7085
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7086
|
+
const person = state.persons.find((row) => row.id === input.personId);
|
|
7087
|
+
if (!person || person.state !== "active" || canonicalNetworkPersonId(person.id, state.persons) !== person.id || person.revision !== input.expectedRevision) {
|
|
7088
|
+
throw new NetworkPersonRegistryError("stale_suppression", "active canonical Person changed; reload before updating suppression");
|
|
7089
|
+
}
|
|
7090
|
+
const policy = { marketingSuppressed: input.marketingSuppressed, discretionarySuppressed: input.discretionarySuppressed, ...input.reason ? { reason: input.reason } : {}, updatedAt: now };
|
|
7091
|
+
const next = { ...person, contactPolicy: policy, revision: person.revision + 1, updatedAt: now };
|
|
7092
|
+
await db.transact([
|
|
7093
|
+
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...next } },
|
|
7094
|
+
eventOp("contact_suppression_changed", mutationId, actor, now, { marketingSuppressed: policy.marketingSuppressed, discretionarySuppressed: policy.discretionarySuppressed }, { personId: person.id })
|
|
7095
|
+
], { mutationId, guards: [{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision } }] });
|
|
7096
|
+
return next;
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
// src/network-person-reveal.ts
|
|
7100
|
+
async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, now = Date.now()) {
|
|
7101
|
+
const reason = input.reason.trim();
|
|
7102
|
+
if (!reason || reason.length > 500) {
|
|
7103
|
+
throw new NetworkPersonRegistryError("invalid_reveal", "a bounded reveal reason is required");
|
|
7104
|
+
}
|
|
7105
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7106
|
+
const target = input.kind === "person" ? state.persons.find((row) => row.id === input.id) : state.aliases.find((row) => row.id === input.id);
|
|
7107
|
+
if (!target) throw new NetworkPersonRegistryError("reveal_not_found", "registry record not found");
|
|
7108
|
+
const ids = input.kind === "person" ? { personId: input.id } : { aliasId: input.id };
|
|
7109
|
+
const event = {
|
|
7110
|
+
t: "update",
|
|
7111
|
+
ns: "networkPersonEvents",
|
|
7112
|
+
id: `network_person_event:${mutationId}`,
|
|
7113
|
+
attrs: {
|
|
7114
|
+
id: `network_person_event:${mutationId}`,
|
|
7115
|
+
kind: "pii_revealed",
|
|
7116
|
+
actorId: actor.id,
|
|
7117
|
+
...ids,
|
|
7118
|
+
mutationId,
|
|
7119
|
+
detail: { targetKind: input.kind, reason },
|
|
7120
|
+
createdAt: now
|
|
7121
|
+
}
|
|
7122
|
+
};
|
|
7123
|
+
await db.transact([event], {
|
|
7124
|
+
mutationId,
|
|
7125
|
+
guards: [{ ns: "networkPersonEvents", id: `network_person_event:${mutationId}`, exists: false }]
|
|
7126
|
+
});
|
|
7127
|
+
return input.kind === "person" ? { kind: "person", person: target, identities: state.claims.filter((claim) => claim.personId === input.id) } : { kind: "alias", alias: target };
|
|
7128
|
+
}
|
|
7129
|
+
|
|
7130
|
+
// src/worker-routes-network-person-registry.ts
|
|
7131
|
+
var PATH2 = "/api/admin/network-person-registry";
|
|
7132
|
+
var MAX_BODY2 = 32e3;
|
|
7133
|
+
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
7134
|
+
function redactedName(displayName) {
|
|
7135
|
+
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7136
|
+
}
|
|
7137
|
+
var redactedLink = (link) => ({
|
|
7138
|
+
id: link.id,
|
|
7139
|
+
personId: link.personId,
|
|
7140
|
+
chapterId: link.chapterId,
|
|
7141
|
+
state: link.state,
|
|
7142
|
+
createdAt: link.createdAt
|
|
7143
|
+
});
|
|
7144
|
+
var safePolicy = (policy) => ({
|
|
7145
|
+
marketingSuppressed: policy.marketingSuppressed,
|
|
7146
|
+
discretionarySuppressed: policy.discretionarySuppressed,
|
|
7147
|
+
hasReason: Boolean(policy.reason),
|
|
7148
|
+
updatedAt: policy.updatedAt
|
|
7149
|
+
});
|
|
7150
|
+
var safeAlias = (alias) => ({
|
|
7151
|
+
id: alias.id,
|
|
7152
|
+
sourceSystem: alias.sourceSystem,
|
|
7153
|
+
displayNameHint: redactedName(alias.displayName),
|
|
7154
|
+
status: alias.status,
|
|
7155
|
+
personId: alias.personId ?? null,
|
|
7156
|
+
candidatePersonIds: alias.candidatePersonIds ?? [],
|
|
7157
|
+
consent: {
|
|
7158
|
+
marketing: alias.consent.marketing,
|
|
7159
|
+
discretionary: alias.consent.discretionary,
|
|
7160
|
+
recordedAt: alias.consent.recordedAt,
|
|
7161
|
+
hasBasis: Boolean(alias.consent.basis)
|
|
7162
|
+
},
|
|
7163
|
+
identityClaims: alias.identityClaims.map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7164
|
+
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
7165
|
+
revision: alias.revision,
|
|
7166
|
+
updatedAt: alias.updatedAt
|
|
7167
|
+
});
|
|
7168
|
+
var safePerson = (person) => ({
|
|
7169
|
+
id: person.id,
|
|
7170
|
+
displayNameHint: redactedName(person.displayName),
|
|
7171
|
+
state: person.state,
|
|
7172
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7173
|
+
revision: person.revision,
|
|
7174
|
+
contactPolicy: safePolicy(person.contactPolicy)
|
|
7175
|
+
});
|
|
7176
|
+
var positiveRevision = (value) => {
|
|
7177
|
+
const parsed = Number(value);
|
|
7178
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
7179
|
+
};
|
|
7180
|
+
async function gate10(req, env, ctx) {
|
|
7181
|
+
const db = ctx.makeDb(env);
|
|
7182
|
+
const user = await ctx.verifyUser(req, env);
|
|
7183
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
7184
|
+
if (!await ctx.isSuperAdminEmail(db, user.email)) return { response: json({ error: "forbidden" }, 403) };
|
|
7185
|
+
return { db, user };
|
|
7186
|
+
}
|
|
7187
|
+
function registryError(error) {
|
|
7188
|
+
if (error instanceof NetworkPersonRegistryError) {
|
|
7189
|
+
const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
|
|
7190
|
+
return json({ error: error.message, code: error.code }, status);
|
|
7191
|
+
}
|
|
7192
|
+
if (error instanceof Error && error.code === "transact_guard_failed") {
|
|
7193
|
+
return json({ error: "registry changed; reload and retry", code: "stale_write" }, 409);
|
|
7194
|
+
}
|
|
7195
|
+
return null;
|
|
7196
|
+
}
|
|
7197
|
+
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
7198
|
+
if (url.pathname !== PATH2) return null;
|
|
7199
|
+
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
7200
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
7201
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
7202
|
+
return json({ error: "request is too large" }, 413);
|
|
7203
|
+
}
|
|
7204
|
+
const got = await gate10(req, env, ctx);
|
|
7205
|
+
if ("response" in got) return got.response;
|
|
7206
|
+
const db = got.db;
|
|
7207
|
+
if (req.method === "POST") {
|
|
7208
|
+
let body;
|
|
7209
|
+
try {
|
|
7210
|
+
const text4 = await req.text();
|
|
7211
|
+
if (new TextEncoder().encode(text4).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
7212
|
+
const parsed = JSON.parse(text4);
|
|
7213
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
7214
|
+
body = parsed;
|
|
7215
|
+
} catch {
|
|
7216
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
7217
|
+
}
|
|
7218
|
+
const command = typeof body.command === "string" ? body.command : "";
|
|
7219
|
+
const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
|
|
7220
|
+
const input = body.input && typeof body.input === "object" && !Array.isArray(body.input) ? body.input : {};
|
|
7221
|
+
if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
|
|
7222
|
+
const actionActor = { id: got.user.userId };
|
|
7223
|
+
try {
|
|
7224
|
+
if (command === "import") {
|
|
7225
|
+
const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
|
|
7226
|
+
if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);
|
|
7227
|
+
const result = await importNetworkPerson(db, validated.value, mutationId, actionActor);
|
|
7228
|
+
return json({
|
|
7229
|
+
result: {
|
|
7230
|
+
status: result.status,
|
|
7231
|
+
alias: safeAlias(result.alias),
|
|
7232
|
+
..."person" in result && result.person ? { person: safePerson(result.person) } : {},
|
|
7233
|
+
..."replayed" in result ? { replayed: result.replayed } : {}
|
|
7234
|
+
}
|
|
7235
|
+
}, 201);
|
|
7236
|
+
}
|
|
7237
|
+
if (command === "review") {
|
|
7238
|
+
if (input.decision !== "link" && input.decision !== "exclude") return json({ error: "review decision must be link or exclude" }, 400);
|
|
7239
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7240
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7241
|
+
if (input.decision === "link" && typeof input.personId !== "string") return json({ error: "personId is required when linking" }, 400);
|
|
7242
|
+
const result = await reviewNetworkPersonAlias(db, {
|
|
7243
|
+
aliasId: typeof input.aliasId === "string" ? input.aliasId : "",
|
|
7244
|
+
expectedRevision,
|
|
7245
|
+
decision: input.decision,
|
|
7246
|
+
...typeof input.personId === "string" ? { personId: input.personId } : {}
|
|
7247
|
+
}, mutationId, actionActor);
|
|
7248
|
+
return json({ result: safeAlias(result) });
|
|
7249
|
+
}
|
|
7250
|
+
if (command === "merge") {
|
|
7251
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7252
|
+
const expectedTargetRevision = positiveRevision(input.expectedTargetRevision);
|
|
7253
|
+
if (!expectedRevision || !expectedTargetRevision) return json({ error: "source and target revisions are required" }, 400);
|
|
7254
|
+
const result = await mergeNetworkPersons(db, {
|
|
7255
|
+
sourcePersonId: typeof input.sourcePersonId === "string" ? input.sourcePersonId : "",
|
|
7256
|
+
targetPersonId: typeof input.targetPersonId === "string" ? input.targetPersonId : "",
|
|
7257
|
+
expectedRevision,
|
|
7258
|
+
expectedTargetRevision
|
|
7259
|
+
}, mutationId, actionActor);
|
|
7260
|
+
return json({ result: { source: safePerson(result.source), target: safePerson(result.target) } });
|
|
7261
|
+
}
|
|
7262
|
+
if (command === "unmerge") {
|
|
7263
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7264
|
+
if (!expectedRevision) return json({ error: "expectedRevision is required" }, 400);
|
|
7265
|
+
const result = await unmergeNetworkPerson(db, {
|
|
7266
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7267
|
+
expectedRevision
|
|
7268
|
+
}, mutationId, actionActor);
|
|
7269
|
+
return json({ result: safePerson(result) });
|
|
7270
|
+
}
|
|
7271
|
+
if (command === "suppression") {
|
|
7272
|
+
const expectedRevision = positiveRevision(input.expectedRevision);
|
|
7273
|
+
if (!expectedRevision || typeof input.marketingSuppressed !== "boolean" || typeof input.discretionarySuppressed !== "boolean") {
|
|
7274
|
+
return json({ error: "expectedRevision and both suppression flags are required" }, 400);
|
|
7275
|
+
}
|
|
7276
|
+
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
7277
|
+
if (reason.length > 500) return json({ error: "suppression reason is too long" }, 400);
|
|
7278
|
+
const result = await suppressNetworkPerson(db, {
|
|
7279
|
+
personId: typeof input.personId === "string" ? input.personId : "",
|
|
7280
|
+
expectedRevision,
|
|
7281
|
+
marketingSuppressed: input.marketingSuppressed,
|
|
7282
|
+
discretionarySuppressed: input.discretionarySuppressed,
|
|
7283
|
+
...reason ? { reason } : {}
|
|
7284
|
+
}, mutationId, actionActor);
|
|
7285
|
+
return json({ result: safePerson(result) });
|
|
7286
|
+
}
|
|
7287
|
+
if (command === "reveal") {
|
|
7288
|
+
if (input.kind !== "person" && input.kind !== "alias") return json({ error: "reveal kind must be person or alias" }, 400);
|
|
7289
|
+
const result = await revealNetworkPersonRegistryRecord(db, {
|
|
7290
|
+
kind: input.kind,
|
|
7291
|
+
id: typeof input.id === "string" ? input.id : "",
|
|
7292
|
+
reason: typeof input.reason === "string" ? input.reason : ""
|
|
7293
|
+
}, mutationId, actionActor);
|
|
7294
|
+
return json({ sensitive: true, result });
|
|
7295
|
+
}
|
|
7296
|
+
return json({ error: "unknown registry command" }, 400);
|
|
7297
|
+
} catch (error) {
|
|
7298
|
+
const response = registryError(error);
|
|
7299
|
+
if (response) return response;
|
|
7300
|
+
throw error;
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
const state = await loadNetworkPersonRegistry(db);
|
|
7304
|
+
const persons = state.persons.map((person) => {
|
|
7305
|
+
const links = state.links.filter((link) => link.personId === person.id);
|
|
7306
|
+
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7307
|
+
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7308
|
+
return {
|
|
7309
|
+
id: person.id,
|
|
7310
|
+
displayNameHint: redactedName(person.displayName),
|
|
7311
|
+
state: person.state,
|
|
7312
|
+
canonicalPersonId: canonicalId,
|
|
7313
|
+
mergedIntoId: person.mergedIntoId ?? null,
|
|
7314
|
+
revision: person.revision,
|
|
7315
|
+
contactPolicy: safePolicy(person.contactPolicy),
|
|
7316
|
+
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7317
|
+
chapterLinks: links.map(redactedLink),
|
|
7318
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, links.map(redactedLink)).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7319
|
+
};
|
|
7320
|
+
});
|
|
7321
|
+
const aliases = state.aliases.map(safeAlias);
|
|
7322
|
+
return json({
|
|
7323
|
+
version: 1,
|
|
7324
|
+
mode: "shadow",
|
|
7325
|
+
writable: true,
|
|
7326
|
+
authoritativeForChapterOperation: false,
|
|
7327
|
+
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7328
|
+
persons,
|
|
7329
|
+
aliases,
|
|
7330
|
+
reviewQueue: aliases.filter((alias) => alias.status === "ambiguous"),
|
|
7331
|
+
excluded: aliases.filter((alias) => alias.status === "excluded"),
|
|
7332
|
+
recentEvents: state.events.slice(0, 100).map((event) => ({
|
|
7333
|
+
id: event.id,
|
|
7334
|
+
kind: event.kind,
|
|
7335
|
+
personId: event.personId ?? null,
|
|
7336
|
+
aliasId: event.aliasId ?? null,
|
|
7337
|
+
actorId: event.actorId,
|
|
7338
|
+
mutationId: event.mutationId,
|
|
7339
|
+
createdAt: event.createdAt
|
|
7340
|
+
}))
|
|
7341
|
+
});
|
|
7342
|
+
};
|
|
7343
|
+
|
|
6754
7344
|
// src/worker-routes-formation.ts
|
|
6755
7345
|
var import_crm11 = require("@odla-ai/crm");
|
|
6756
7346
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
@@ -7065,17 +7655,17 @@ async function builderLibraryDigest(document) {
|
|
|
7065
7655
|
var control2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7066
7656
|
var local2 = (row) => row?.document && typeof row.document === "object" ? row : null;
|
|
7067
7657
|
async function loadActiveBuilderLibraryControls(db, targetId, environment) {
|
|
7068
|
-
const
|
|
7069
|
-
return Array.isArray(
|
|
7658
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
|
|
7659
|
+
return Array.isArray(rows5) ? rows5.map(control2).filter(Boolean) : [];
|
|
7070
7660
|
}
|
|
7071
7661
|
async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environment) {
|
|
7072
|
-
const
|
|
7073
|
-
return control2(Array.isArray(
|
|
7662
|
+
const rows5 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
|
|
7663
|
+
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7074
7664
|
}
|
|
7075
7665
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
7076
7666
|
const id = `builder-library-local:${environment}`;
|
|
7077
|
-
const
|
|
7078
|
-
return local2(Array.isArray(
|
|
7667
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
|
|
7668
|
+
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
7079
7669
|
}
|
|
7080
7670
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
7081
7671
|
const { document, digest } = envelope;
|
|
@@ -7134,9 +7724,10 @@ async function verifySource(req, db, ctx) {
|
|
|
7134
7724
|
return null;
|
|
7135
7725
|
}
|
|
7136
7726
|
var handleBuilderLibraryControl = async (req, url, env, ctx) => {
|
|
7137
|
-
if (
|
|
7727
|
+
if (url.pathname !== BUILDER_LIBRARY_CONTROL_PATH) return null;
|
|
7138
7728
|
const settings = ctx.chapter.builderLibrary;
|
|
7139
7729
|
if (!settings) return json({ error: "not found" }, 404);
|
|
7730
|
+
if (req.method !== "POST") return json({ error: "unauthorized" }, 401);
|
|
7140
7731
|
const db = ctx.makeDb(env);
|
|
7141
7732
|
const verified = await verifySource(req, db, ctx);
|
|
7142
7733
|
if (!verified) return json({ error: "unauthorized" }, 401);
|
|
@@ -7565,6 +8156,7 @@ var BUILTIN_ROUTES = [
|
|
|
7565
8156
|
handleAdminNetworkRecords,
|
|
7566
8157
|
handleAdminNetworkNotes,
|
|
7567
8158
|
handleAdminNetworkSharedNotes,
|
|
8159
|
+
handleAdminNetworkPersonRegistry,
|
|
7568
8160
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
7569
8161
|
// get first refusal through options.routes, then this terminates unknown API
|
|
7570
8162
|
// paths with an explicit machine-readable 404.
|