@odla-ai/chapter 0.47.5 → 0.47.6

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.
@@ -6894,6 +6894,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
6894
6894
  // src/network-person-registry.ts
6895
6895
  var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
6896
6896
  var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
6897
+ var FIXTURE_MARKER = /fixture|(?:^|[\s._:-])(dummy|sample|test(?:case)?)(?:$|[\s._:-])/i;
6897
6898
  function normalizeNetworkIdentity(kind, value) {
6898
6899
  const trimmed = value.trim();
6899
6900
  if (kind === "email") return trimmed.toLowerCase();
@@ -6920,6 +6921,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
6920
6921
  if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
6921
6922
  if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
6922
6923
  if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
6924
+ if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
6925
+ errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
6926
+ }
6923
6927
  const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
6924
6928
  const consentStates = ["unknown", "granted", "denied"];
6925
6929
  const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
@@ -7332,6 +7336,247 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
7332
7336
  return next;
7333
7337
  }
7334
7338
 
7339
+ // src/network-person-bulk-preview.ts
7340
+ var NETWORK_PERSON_BULK_IMPORT_MAX_ROWS = 5e3;
7341
+ var NETWORK_PERSON_BULK_IMPORT_VERSION = 1;
7342
+ var networkPersonBulkEmptyCounts = () => ({
7343
+ new: 0,
7344
+ replay: 0,
7345
+ exactMatchReview: 0,
7346
+ invalid: 0,
7347
+ rejected: 0,
7348
+ failed: 0
7349
+ });
7350
+ var incrementNetworkPersonBulkCount = (counts, status) => {
7351
+ if (status === "exact_match_review") counts.exactMatchReview += 1;
7352
+ else counts[status] += 1;
7353
+ };
7354
+ function stableValue(value) {
7355
+ if (Array.isArray(value)) return value.map(stableValue);
7356
+ if (value && typeof value === "object") {
7357
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
7358
+ }
7359
+ return value;
7360
+ }
7361
+ async function networkPersonBulkSha256(value) {
7362
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
7363
+ return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
7364
+ }
7365
+ function assertNetworkPersonBulkRows(rows5) {
7366
+ if (!Array.isArray(rows5) || rows5.length < 1) {
7367
+ throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
7368
+ }
7369
+ if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
7370
+ throw new NetworkPersonRegistryError(
7371
+ "bulk_rows_exceeded",
7372
+ `bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
7373
+ );
7374
+ }
7375
+ return rows5;
7376
+ }
7377
+ async function networkPersonBulkBatchDigest(rows5) {
7378
+ const bounded = assertNetworkPersonBulkRows(rows5);
7379
+ return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
7380
+ }
7381
+ async function networkPersonRegistryStateDigest(state) {
7382
+ const snapshot = {
7383
+ persons: state.persons.map(({ id, revision, state: personState, mergedIntoId }) => ({ id, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
7384
+ claims: state.claims.map(({ id, personId }) => ({ id, personId })).sort((a, b) => a.id.localeCompare(b.id)),
7385
+ aliases: state.aliases.map(({ id, revision, status, personId }) => ({ id, revision, status, personId })).sort((a, b) => a.id.localeCompare(b.id)),
7386
+ links: state.links.map(({ id, personId, state: linkState }) => ({ id, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
7387
+ };
7388
+ return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
7389
+ }
7390
+ function rejectedValidation(errors) {
7391
+ return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
7392
+ }
7393
+ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
7394
+ const aliasIds = new Set(state.aliases.map((alias) => alias.id));
7395
+ const batchAliases = /* @__PURE__ */ new Set();
7396
+ const claimOwners = /* @__PURE__ */ new Map();
7397
+ for (const claim of state.claims) {
7398
+ const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
7399
+ owners.add(claim.personId);
7400
+ claimOwners.set(claim.id, owners);
7401
+ }
7402
+ const results = [];
7403
+ const values = [];
7404
+ for (let index2 = 0; index2 < rows5.length; index2 += 1) {
7405
+ const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
7406
+ if (!validation.ok || !validation.value) {
7407
+ const errors = validation.errors.slice(0, 8);
7408
+ results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
7409
+ values.push(null);
7410
+ continue;
7411
+ }
7412
+ const value = validation.value;
7413
+ const aliasId = await networkSourceKey(value.sourceSystem, value.sourceRecordId);
7414
+ if (batchAliases.has(aliasId)) {
7415
+ results.push({ rowNumber: index2 + 2, status: "rejected", errors: ["duplicate source record in this batch"] });
7416
+ values.push(null);
7417
+ continue;
7418
+ }
7419
+ batchAliases.add(aliasId);
7420
+ if (aliasIds.has(aliasId)) {
7421
+ results.push({ rowNumber: index2 + 2, status: "replay" });
7422
+ values.push(value);
7423
+ continue;
7424
+ }
7425
+ const claims = await Promise.all(value.identities.map(networkIdentityClaim));
7426
+ const candidates = new Set(claims.flatMap((claim) => [...claimOwners.get(claim.id) ?? []]));
7427
+ results.push(candidates.size ? { rowNumber: index2 + 2, status: "exact_match_review", candidateCount: candidates.size } : { rowNumber: index2 + 2, status: "new" });
7428
+ const plannedOwner = `bulk_row_${index2 + 2}`;
7429
+ for (const claim of claims) {
7430
+ const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
7431
+ owners.add(plannedOwner);
7432
+ claimOwners.set(claim.id, owners);
7433
+ }
7434
+ values.push(value);
7435
+ }
7436
+ return { rows: results, values };
7437
+ }
7438
+ async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
7439
+ const bounded = assertNetworkPersonBulkRows(rows5);
7440
+ const state = await loadNetworkPersonRegistry(db);
7441
+ const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
7442
+ const counts = networkPersonBulkEmptyCounts();
7443
+ for (const row of classified.rows) incrementNetworkPersonBulkCount(counts, row.status);
7444
+ return {
7445
+ version: NETWORK_PERSON_BULK_IMPORT_VERSION,
7446
+ batchDigest: await networkPersonBulkBatchDigest(bounded),
7447
+ stateDigest: await networkPersonRegistryStateDigest(state),
7448
+ rowCount: bounded.length,
7449
+ counts,
7450
+ rows: classified.rows
7451
+ };
7452
+ }
7453
+
7454
+ // src/network-person-bulk-import.ts
7455
+ async function derivedMutationId(batchMutationId, part) {
7456
+ return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
7457
+ }
7458
+ function matchingEvent(events, mutationId, kind) {
7459
+ return events.find((event) => event.mutationId === mutationId && event.kind === kind);
7460
+ }
7461
+ function eventString(event, key) {
7462
+ return typeof event.detail[key] === "string" ? String(event.detail[key]) : "";
7463
+ }
7464
+ function eventCounts(event) {
7465
+ const value = event.detail.counts;
7466
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
7467
+ }
7468
+ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
7469
+ if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
7470
+ const rows5 = assertNetworkPersonBulkRows(input.rows);
7471
+ const batchDigest = await networkPersonBulkBatchDigest(rows5);
7472
+ if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
7473
+ throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
7474
+ }
7475
+ if (!/^[a-f0-9]{64}$/.test(input.expectedStateDigest)) {
7476
+ throw new NetworkPersonRegistryError("bulk_state_digest_required", "a reviewed registry state digest is required");
7477
+ }
7478
+ const startedMutationId = await derivedMutationId(batchMutationId, "started");
7479
+ const completedMutationId = await derivedMutationId(batchMutationId, "completed");
7480
+ let state = await loadNetworkPersonRegistry(db);
7481
+ const completed = matchingEvent(state.events, completedMutationId, "bulk_import_completed");
7482
+ if (completed) {
7483
+ if (eventString(completed, "batchDigest") !== batchDigest) {
7484
+ throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id was already used for different rows");
7485
+ }
7486
+ return {
7487
+ version: NETWORK_PERSON_BULK_IMPORT_VERSION,
7488
+ batchDigest,
7489
+ stateDigest: eventString(completed, "stateDigest"),
7490
+ rowCount: Number(completed.detail.rowCount) || rows5.length,
7491
+ counts: eventCounts(completed),
7492
+ rows: [],
7493
+ replayed: true
7494
+ };
7495
+ }
7496
+ let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
7497
+ if (!started) {
7498
+ const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
7499
+ if (preview.stateDigest !== input.expectedStateDigest) {
7500
+ throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
7501
+ }
7502
+ try {
7503
+ await db.transact([
7504
+ networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
7505
+ batchDigest,
7506
+ stateDigest: input.expectedStateDigest,
7507
+ rowCount: rows5.length,
7508
+ previewCounts: preview.counts
7509
+ })
7510
+ ], {
7511
+ mutationId: startedMutationId,
7512
+ guards: [{ ns: "networkPersonEvents", id: `network_person_event:${startedMutationId}`, exists: false }]
7513
+ });
7514
+ } catch (error) {
7515
+ if (!(error instanceof Error) || error.code !== "transact_guard_failed") throw error;
7516
+ }
7517
+ state = await loadNetworkPersonRegistry(db);
7518
+ started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
7519
+ }
7520
+ if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
7521
+ throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
7522
+ }
7523
+ const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
7524
+ const results = [];
7525
+ const counts = networkPersonBulkEmptyCounts();
7526
+ for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
7527
+ const row = classified.rows[index2];
7528
+ const value = classified.values[index2];
7529
+ if (!value) {
7530
+ results.push(row);
7531
+ incrementNetworkPersonBulkCount(counts, row.status);
7532
+ continue;
7533
+ }
7534
+ try {
7535
+ const result = await importNetworkPerson(
7536
+ db,
7537
+ value,
7538
+ await derivedMutationId(batchMutationId, `row_${index2 + 2}`),
7539
+ actor,
7540
+ now + index2 + 1
7541
+ );
7542
+ const status = "replayed" in result && result.replayed ? "replay" : result.status === "ambiguous" ? "exact_match_review" : "new";
7543
+ const applied = {
7544
+ rowNumber: index2 + 2,
7545
+ status,
7546
+ ...result.status === "ambiguous" ? { candidateCount: result.alias.candidatePersonIds?.length ?? 0 } : {}
7547
+ };
7548
+ results.push(applied);
7549
+ incrementNetworkPersonBulkCount(counts, status);
7550
+ } catch (error) {
7551
+ const failed = {
7552
+ rowNumber: index2 + 2,
7553
+ status: "failed",
7554
+ errors: [error instanceof Error ? error.message : "row failed"]
7555
+ };
7556
+ results.push(failed);
7557
+ incrementNetworkPersonBulkCount(counts, failed.status);
7558
+ }
7559
+ }
7560
+ if (counts.failed === 0) {
7561
+ await db.transact([
7562
+ networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
7563
+ batchDigest,
7564
+ stateDigest: input.expectedStateDigest,
7565
+ rowCount: rows5.length,
7566
+ counts
7567
+ })
7568
+ ], { mutationId: completedMutationId });
7569
+ }
7570
+ return {
7571
+ version: NETWORK_PERSON_BULK_IMPORT_VERSION,
7572
+ batchDigest,
7573
+ stateDigest: input.expectedStateDigest,
7574
+ rowCount: rows5.length,
7575
+ counts,
7576
+ rows: results
7577
+ };
7578
+ }
7579
+
7335
7580
  // src/network-person-route-projection.ts
7336
7581
  function redactedName(displayName) {
7337
7582
  return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
@@ -7448,7 +7693,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
7448
7693
 
7449
7694
  // src/worker-routes-network-person-registry.ts
7450
7695
  var PATH2 = "/api/admin/network-person-registry";
7451
- var MAX_BODY2 = 32e3;
7696
+ var MAX_BODY2 = 8 * 1024 * 1024;
7452
7697
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
7453
7698
  var positiveRevision = (value) => {
7454
7699
  const parsed = Number(value);
@@ -7463,7 +7708,7 @@ async function gate10(req, env, ctx) {
7463
7708
  }
7464
7709
  function registryError(error) {
7465
7710
  if (error instanceof NetworkPersonRegistryError) {
7466
- const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
7711
+ const status = error.code.includes("stale") || error.code === "identity_conflict" || error.code === "bulk_state_changed" || error.code === "bulk_digest_changed" || error.code === "bulk_mutation_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
7467
7712
  return json({ error: error.message, code: error.code }, status);
7468
7713
  }
7469
7714
  if (error instanceof Error && error.code === "transact_guard_failed") {
@@ -7498,6 +7743,23 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
7498
7743
  if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
7499
7744
  const actionActor = { id: got.user.userId };
7500
7745
  try {
7746
+ if (command === "preview-bulk-import") {
7747
+ const result = await previewNetworkPersonBulkImport(
7748
+ db,
7749
+ input.rows,
7750
+ ctx.chapter.networkPersonRegistry.chapterIds
7751
+ );
7752
+ return json({ result });
7753
+ }
7754
+ if (command === "apply-bulk-import") {
7755
+ const result = await applyNetworkPersonBulkImport(db, {
7756
+ rows: input.rows,
7757
+ expectedBatchDigest: typeof input.expectedBatchDigest === "string" ? input.expectedBatchDigest : "",
7758
+ expectedStateDigest: typeof input.expectedStateDigest === "string" ? input.expectedStateDigest : "",
7759
+ reviewed: input.reviewed === true
7760
+ }, mutationId, actionActor, ctx.chapter.networkPersonRegistry.chapterIds);
7761
+ return json({ result }, result.replayed ? 200 : 201);
7762
+ }
7501
7763
  if (command === "import") {
7502
7764
  const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
7503
7765
  if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);