@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.
@@ -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, rows4 = []) {
340
- const stored = rows4.filter((tier) => tier.active);
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 rows4;
386
+ let rows5;
387
387
  try {
388
- rows4 = (await db.query({
388
+ rows5 = (await db.query({
389
389
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
390
390
  })).tiers;
391
391
  } catch {
392
- rows4 = void 0;
392
+ rows5 = void 0;
393
393
  }
394
- const list = Array.isArray(rows4) ? rows4.map((row) => rowToTier(row, groupId)) : [];
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 rows4 = (await db.query({ signupControls: {} })).signupControls;
1454
- return Array.isArray(rows4) && rows4.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
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 rows4 = (await db.query({ signupControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
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 rows4 = (await db.query({ signupControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
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 rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
2850
- return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
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 rows4 = (await db.query({ memberContentControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
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 rows4 = (await db.query({ memberContentControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
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 rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
2875
- return local(Array.isArray(rows4) ? rows4[0] : void 0);
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 rows4 = (await db.query({ applications: { $: {
2930
+ let rows5 = (await db.query({ applications: { $: {
2931
2931
  where: { emailNormalized: normalizeAdmissionEmail(email) },
2932
2932
  limit: 20
2933
2933
  } } })).applications;
2934
- if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
2935
- const latest = Array.isArray(rows4) ? [...rows4].filter((row) => String(row.groupId ?? chapterId) === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
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 rows4 = res[ns];
3662
- return Array.isArray(rows4) ? rows4[0] : void 0;
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 rows4 = charges.body.data ?? [];
4063
- const paid = rows4.filter((c) => c.status === "succeeded" && c.refunded !== true);
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(rows4, now) {
4172
- return rows4.map((row) => {
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 rows4 = (await db.query({ [ns]: { $: q } }))[ns];
4465
- return Array.isArray(rows4) ? rows4[0] : void 0;
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 rows4 = res[ns];
4536
- return Array.isArray(rows4) ? rows4[0] : void 0;
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(rows4) {
5003
- return rows4.map((m) => ({
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 rows4 = Array.isArray(query.meetings) ? query.meetings : [];
5026
- if (Number.isFinite(from)) rows4 = rows4.filter((m) => Number(m.startAt ?? 0) >= from);
5027
- if (Number.isFinite(to)) rows4 = rows4.filter((m) => Number(m.startAt ?? 0) <= 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(rows4), await upcomingEvents(env), Date.now());
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 = rows4.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
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 gate10 = await adminGate(req, env, ctx);
5079
- if (gate10 instanceof Response) return gate10;
5080
- const result = await backfillCrm(crmDeps2(gate10.db, ctx));
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 gate10 = await adminGate(req, env, ctx);
5086
- if (gate10 instanceof Response) return gate10;
5087
- const { db } = gate10;
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 rows4 = [...people.values()].sort(
5133
+ const rows5 = [...people.values()].sort(
5134
5134
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
5135
5135
  );
5136
- return json({ people: rows4 });
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 gate10 = await adminGate(req, env, ctx);
5141
- if (gate10 instanceof Response) return gate10;
5142
- const { db } = gate10;
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 gate10 = await adminGate(req, env, ctx);
5154
- if (gate10 instanceof Response) return gate10;
5155
- const { db, actor } = gate10;
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 rows4 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
7043
- return Array.isArray(rows4) ? rows4.map(control2).filter(Boolean) : [];
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 rows4 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
7047
- return control2(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
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 rows4 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
7052
- return local2(Array.isArray(rows4) ? rows4[0] : void 0);
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 (req.method !== "POST" || url.pathname !== BUILDER_LIBRARY_CONTROL_PATH) return null;
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.