@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.
@@ -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, rows4 = []) {
375
- const stored = rows4.filter((tier) => tier.active);
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 rows4;
421
+ let rows5;
422
422
  try {
423
- rows4 = (await db.query({
423
+ rows5 = (await db.query({
424
424
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
425
425
  })).tiers;
426
426
  } catch {
427
- rows4 = void 0;
427
+ rows5 = void 0;
428
428
  }
429
- const list = Array.isArray(rows4) ? rows4.map((row) => rowToTier(row, groupId)) : [];
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 rows4 = (await db.query({ signupControls: {} })).signupControls;
1484
- return Array.isArray(rows4) && rows4.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
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 rows4 = (await db.query({ signupControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
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 rows4 = (await db.query({ signupControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
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 rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
2880
- return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
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 rows4 = (await db.query({ memberContentControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
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 rows4 = (await db.query({ memberContentControls: { $: {
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(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
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 rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
2905
- return local(Array.isArray(rows4) ? rows4[0] : void 0);
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 rows4 = (await db.query({ applications: { $: {
2960
+ let rows5 = (await db.query({ applications: { $: {
2961
2961
  where: { emailNormalized: normalizeAdmissionEmail(email) },
2962
2962
  limit: 20
2963
2963
  } } })).applications;
2964
- if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
2965
- 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;
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 rows4 = res[ns];
3692
- return Array.isArray(rows4) ? rows4[0] : void 0;
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 rows4 = charges.body.data ?? [];
4093
- const paid = rows4.filter((c) => c.status === "succeeded" && c.refunded !== true);
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(rows4, now) {
4202
- return rows4.map((row) => {
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 rows4 = (await db.query({ [ns]: { $: q } }))[ns];
4495
- return Array.isArray(rows4) ? rows4[0] : void 0;
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 rows4 = res[ns];
4566
- return Array.isArray(rows4) ? rows4[0] : void 0;
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(rows4) {
5033
- return rows4.map((m) => ({
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 rows4 = Array.isArray(query.meetings) ? query.meetings : [];
5056
- if (Number.isFinite(from)) rows4 = rows4.filter((m) => Number(m.startAt ?? 0) >= from);
5057
- if (Number.isFinite(to)) rows4 = rows4.filter((m) => Number(m.startAt ?? 0) <= 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(rows4), await upcomingEvents(env), Date.now());
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 = rows4.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
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 gate10 = await adminGate(req, env, ctx);
5109
- if (gate10 instanceof Response) return gate10;
5110
- const result = await backfillCrm(crmDeps2(gate10.db, ctx));
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 gate10 = await adminGate(req, env, ctx);
5116
- if (gate10 instanceof Response) return gate10;
5117
- const { db } = gate10;
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 rows4 = [...people.values()].sort(
5163
+ const rows5 = [...people.values()].sort(
5164
5164
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
5165
5165
  );
5166
- return json({ people: rows4 });
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 gate10 = await adminGate(req, env, ctx);
5171
- if (gate10 instanceof Response) return gate10;
5172
- const { db } = gate10;
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 gate10 = await adminGate(req, env, ctx);
5184
- if (gate10 instanceof Response) return gate10;
5185
- const { db, actor } = gate10;
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 rows4 = (await db.query({ builderLibraryControls: { $: { where: { targetId, environment, active: true }, limit: 500 } } })).builderLibraryControls;
7069
- return Array.isArray(rows4) ? rows4.map(control2).filter(Boolean) : [];
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 rows4 = (await db.query({ builderLibraryControls: { $: { where: { sourceId, targetId, environment }, order: { revision: "desc" }, limit: 20 } } })).builderLibraryControls;
7073
- return control2(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
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 rows4 = (await db.query({ builderLibraryLocal: { $: { where: { id }, limit: 1 } } })).builderLibraryLocal;
7078
- return local2(Array.isArray(rows4) ? rows4[0] : void 0);
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 (req.method !== "POST" || url.pathname !== BUILDER_LIBRARY_CONTROL_PATH) return null;
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.