@odla-ai/chapter 0.47.5 → 0.47.7

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.
@@ -351,6 +351,8 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
351
351
  name: group.name,
352
352
  standardPriceCents: group.standardPriceCents ?? 0,
353
353
  foundingDiscountCents: group.foundingDiscountCents ?? 0,
354
+ currency: group.currency ?? "usd",
355
+ interval: group.interval ?? "year",
354
356
  tiers: [...tiers],
355
357
  // The browser evaluates the same conditions the server enforces.
356
358
  conditions,
@@ -383,6 +385,8 @@ function resolveTiers(group, rows5 = []) {
383
385
  groupId: String(group.id ?? ""),
384
386
  name: "Membership",
385
387
  priceCents: group.standardPriceCents ?? 0,
388
+ currency: group.currency ?? "usd",
389
+ interval: group.interval ?? "year",
386
390
  ...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
387
391
  sortOrder: 0,
388
392
  active: true
@@ -395,11 +399,13 @@ function findTier(tiers, tierId) {
395
399
  if (!tierId) return tiers.length === 1 ? tiers[0] : null;
396
400
  return tiers.find((tier) => tier.id === tierId) ?? null;
397
401
  }
398
- function joinConfigTiers(tiers) {
402
+ function joinConfigTiers(tiers, defaults = {}) {
399
403
  return tiers.map((tier) => ({
400
404
  id: tier.id,
401
405
  name: tier.name,
402
406
  priceCents: tier.priceCents,
407
+ currency: tier.currency ?? defaults.currency ?? "usd",
408
+ interval: tier.interval ?? defaults.interval ?? "year",
403
409
  blurb: tier.blurb ?? "",
404
410
  free: tierIsFree(tier)
405
411
  }));
@@ -412,6 +418,8 @@ function rowToTier(row, groupId) {
412
418
  groupId: String(row.groupId ?? groupId),
413
419
  name: String(row.name ?? ""),
414
420
  priceCents: typeof row.priceCents === "number" ? row.priceCents : 0,
421
+ ...typeof row.currency === "string" && row.currency ? { currency: row.currency } : {},
422
+ ...row.interval === "year" || row.interval === "month" ? { interval: row.interval } : {},
415
423
  ...typeof row.stripePriceId === "string" && row.stripePriceId ? { stripePriceId: row.stripePriceId } : {},
416
424
  ...typeof row.blurb === "string" ? { blurb: row.blurb } : {},
417
425
  sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
@@ -1521,6 +1529,8 @@ function groupAttrs(document) {
1521
1529
  const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1522
1530
  return {
1523
1531
  ...first4 ? { standardPriceCents: first4.priceCents } : {},
1532
+ currency: first4?.currency ?? "usd",
1533
+ interval: first4?.interval ?? "year",
1524
1534
  foundingDiscountCents: 0,
1525
1535
  ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1526
1536
  ...document.policy ?? {},
@@ -1554,6 +1564,8 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
1554
1564
  groupId: document.target.chapterId,
1555
1565
  name: tier.name,
1556
1566
  priceCents: tier.priceCents,
1567
+ currency: tier.currency ?? "usd",
1568
+ interval: tier.interval ?? "year",
1557
1569
  ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
1558
1570
  ...tier.blurb ? { blurb: tier.blurb } : {},
1559
1571
  sortOrder: tier.sortOrder,
@@ -1970,7 +1982,7 @@ var handleMember = async (req, url, env, ctx) => {
1970
1982
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
1971
1983
  const application = controlledApplication(chapter.application, activeControl?.document);
1972
1984
  return json({
1973
- ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1985
+ ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
1974
1986
  fieldContract: chapterFlowFieldContract(chapter, "join", application),
1975
1987
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1976
1988
  ...activeControl ? { signupControl: {
@@ -2327,7 +2339,7 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2327
2339
  "theme",
2328
2340
  "giftOffers"
2329
2341
  ]);
2330
- var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2342
+ var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
2331
2343
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2332
2344
  function unknownKeys(value, allowed2, path, errors) {
2333
2345
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
@@ -2371,6 +2383,12 @@ function tiersOf(value, errors) {
2371
2383
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2372
2384
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2373
2385
  }
2386
+ if (item.currency !== void 0 && (typeof item.currency !== "string" || !/^[a-z]{3}$/.test(item.currency))) {
2387
+ errors.push(`${path}.currency must be a lowercase ISO currency code`);
2388
+ }
2389
+ if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
2390
+ errors.push(`${path}.interval must be year or month`);
2391
+ }
2374
2392
  const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2375
2393
  return [{
2376
2394
  id,
@@ -2378,6 +2396,8 @@ function tiersOf(value, errors) {
2378
2396
  available: item.available === true,
2379
2397
  sortOrder: Number(item.sortOrder) || 0,
2380
2398
  priceCents: Number(item.priceCents) || 0,
2399
+ ...typeof item.currency === "string" ? { currency: item.currency } : {},
2400
+ ...item.interval === "year" || item.interval === "month" ? { interval: item.interval } : {},
2381
2401
  ...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
2382
2402
  ...blurb ? { blurb } : {}
2383
2403
  }];
@@ -6894,6 +6914,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
6894
6914
  // src/network-person-registry.ts
6895
6915
  var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
6896
6916
  var CHAPTER_ID = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
6917
+ var FIXTURE_MARKER = /fixture|(?:^|[\s._:-])(dummy|sample|test(?:case)?)(?:$|[\s._:-])/i;
6897
6918
  function normalizeNetworkIdentity(kind, value) {
6898
6919
  const trimmed = value.trim();
6899
6920
  if (kind === "email") return trimmed.toLowerCase();
@@ -6920,6 +6941,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
6920
6941
  if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
6921
6942
  if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
6922
6943
  if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
6944
+ if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
6945
+ errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
6946
+ }
6923
6947
  const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
6924
6948
  const consentStates = ["unknown", "granted", "denied"];
6925
6949
  const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
@@ -7332,6 +7356,247 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
7332
7356
  return next;
7333
7357
  }
7334
7358
 
7359
+ // src/network-person-bulk-preview.ts
7360
+ var NETWORK_PERSON_BULK_IMPORT_MAX_ROWS = 5e3;
7361
+ var NETWORK_PERSON_BULK_IMPORT_VERSION = 1;
7362
+ var networkPersonBulkEmptyCounts = () => ({
7363
+ new: 0,
7364
+ replay: 0,
7365
+ exactMatchReview: 0,
7366
+ invalid: 0,
7367
+ rejected: 0,
7368
+ failed: 0
7369
+ });
7370
+ var incrementNetworkPersonBulkCount = (counts, status) => {
7371
+ if (status === "exact_match_review") counts.exactMatchReview += 1;
7372
+ else counts[status] += 1;
7373
+ };
7374
+ function stableValue(value) {
7375
+ if (Array.isArray(value)) return value.map(stableValue);
7376
+ if (value && typeof value === "object") {
7377
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
7378
+ }
7379
+ return value;
7380
+ }
7381
+ async function networkPersonBulkSha256(value) {
7382
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
7383
+ return [...new Uint8Array(bytes)].map((part) => part.toString(16).padStart(2, "0")).join("");
7384
+ }
7385
+ function assertNetworkPersonBulkRows(rows5) {
7386
+ if (!Array.isArray(rows5) || rows5.length < 1) {
7387
+ throw new NetworkPersonRegistryError("bulk_rows_required", "bulk import requires at least one row");
7388
+ }
7389
+ if (rows5.length > NETWORK_PERSON_BULK_IMPORT_MAX_ROWS) {
7390
+ throw new NetworkPersonRegistryError(
7391
+ "bulk_rows_exceeded",
7392
+ `bulk import is limited to ${NETWORK_PERSON_BULK_IMPORT_MAX_ROWS.toLocaleString()} rows`
7393
+ );
7394
+ }
7395
+ return rows5;
7396
+ }
7397
+ async function networkPersonBulkBatchDigest(rows5) {
7398
+ const bounded = assertNetworkPersonBulkRows(rows5);
7399
+ return networkPersonBulkSha256(JSON.stringify(stableValue({ version: NETWORK_PERSON_BULK_IMPORT_VERSION, rows: bounded })));
7400
+ }
7401
+ async function networkPersonRegistryStateDigest(state) {
7402
+ const snapshot = {
7403
+ persons: state.persons.map(({ id, revision, state: personState, mergedIntoId }) => ({ id, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
7404
+ claims: state.claims.map(({ id, personId }) => ({ id, personId })).sort((a, b) => a.id.localeCompare(b.id)),
7405
+ aliases: state.aliases.map(({ id, revision, status, personId }) => ({ id, revision, status, personId })).sort((a, b) => a.id.localeCompare(b.id)),
7406
+ links: state.links.map(({ id, personId, state: linkState }) => ({ id, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
7407
+ };
7408
+ return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
7409
+ }
7410
+ function rejectedValidation(errors) {
7411
+ return errors.some((error) => /approved|real person|non-development|development or invalid email|fixture, test, sample/i.test(error));
7412
+ }
7413
+ async function classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state) {
7414
+ const aliasIds = new Set(state.aliases.map((alias) => alias.id));
7415
+ const batchAliases = /* @__PURE__ */ new Set();
7416
+ const claimOwners = /* @__PURE__ */ new Map();
7417
+ for (const claim of state.claims) {
7418
+ const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
7419
+ owners.add(claim.personId);
7420
+ claimOwners.set(claim.id, owners);
7421
+ }
7422
+ const results = [];
7423
+ const values = [];
7424
+ for (let index2 = 0; index2 < rows5.length; index2 += 1) {
7425
+ const validation = validateNetworkPersonImport(rows5[index2], allowedChapterIds);
7426
+ if (!validation.ok || !validation.value) {
7427
+ const errors = validation.errors.slice(0, 8);
7428
+ results.push({ rowNumber: index2 + 2, status: rejectedValidation(errors) ? "rejected" : "invalid", errors });
7429
+ values.push(null);
7430
+ continue;
7431
+ }
7432
+ const value = validation.value;
7433
+ const aliasId = await networkSourceKey(value.sourceSystem, value.sourceRecordId);
7434
+ if (batchAliases.has(aliasId)) {
7435
+ results.push({ rowNumber: index2 + 2, status: "rejected", errors: ["duplicate source record in this batch"] });
7436
+ values.push(null);
7437
+ continue;
7438
+ }
7439
+ batchAliases.add(aliasId);
7440
+ if (aliasIds.has(aliasId)) {
7441
+ results.push({ rowNumber: index2 + 2, status: "replay" });
7442
+ values.push(value);
7443
+ continue;
7444
+ }
7445
+ const claims = await Promise.all(value.identities.map(networkIdentityClaim));
7446
+ const candidates = new Set(claims.flatMap((claim) => [...claimOwners.get(claim.id) ?? []]));
7447
+ results.push(candidates.size ? { rowNumber: index2 + 2, status: "exact_match_review", candidateCount: candidates.size } : { rowNumber: index2 + 2, status: "new" });
7448
+ const plannedOwner = `bulk_row_${index2 + 2}`;
7449
+ for (const claim of claims) {
7450
+ const owners = claimOwners.get(claim.id) ?? /* @__PURE__ */ new Set();
7451
+ owners.add(plannedOwner);
7452
+ claimOwners.set(claim.id, owners);
7453
+ }
7454
+ values.push(value);
7455
+ }
7456
+ return { rows: results, values };
7457
+ }
7458
+ async function previewNetworkPersonBulkImport(db, rows5, allowedChapterIds) {
7459
+ const bounded = assertNetworkPersonBulkRows(rows5);
7460
+ const state = await loadNetworkPersonRegistry(db);
7461
+ const classified = await classifyNetworkPersonBulkRows(bounded, allowedChapterIds, state);
7462
+ const counts = networkPersonBulkEmptyCounts();
7463
+ for (const row of classified.rows) incrementNetworkPersonBulkCount(counts, row.status);
7464
+ return {
7465
+ version: NETWORK_PERSON_BULK_IMPORT_VERSION,
7466
+ batchDigest: await networkPersonBulkBatchDigest(bounded),
7467
+ stateDigest: await networkPersonRegistryStateDigest(state),
7468
+ rowCount: bounded.length,
7469
+ counts,
7470
+ rows: classified.rows
7471
+ };
7472
+ }
7473
+
7474
+ // src/network-person-bulk-import.ts
7475
+ async function derivedMutationId(batchMutationId, part) {
7476
+ return `bulk_${await networkPersonBulkSha256(`${batchMutationId}\0${part}`)}`;
7477
+ }
7478
+ function matchingEvent(events, mutationId, kind) {
7479
+ return events.find((event) => event.mutationId === mutationId && event.kind === kind);
7480
+ }
7481
+ function eventString(event, key) {
7482
+ return typeof event.detail[key] === "string" ? String(event.detail[key]) : "";
7483
+ }
7484
+ function eventCounts(event) {
7485
+ const value = event.detail.counts;
7486
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...networkPersonBulkEmptyCounts(), ...value } : networkPersonBulkEmptyCounts();
7487
+ }
7488
+ async function applyNetworkPersonBulkImport(db, input, batchMutationId, actor, allowedChapterIds, now = Date.now()) {
7489
+ if (input.reviewed !== true) throw new NetworkPersonRegistryError("bulk_review_required", "the exact preview must be explicitly reviewed before apply");
7490
+ const rows5 = assertNetworkPersonBulkRows(input.rows);
7491
+ const batchDigest = await networkPersonBulkBatchDigest(rows5);
7492
+ if (!/^[a-f0-9]{64}$/.test(input.expectedBatchDigest) || batchDigest !== input.expectedBatchDigest) {
7493
+ throw new NetworkPersonRegistryError("bulk_digest_changed", "bulk import rows changed after preview; preview again");
7494
+ }
7495
+ if (!/^[a-f0-9]{64}$/.test(input.expectedStateDigest)) {
7496
+ throw new NetworkPersonRegistryError("bulk_state_digest_required", "a reviewed registry state digest is required");
7497
+ }
7498
+ const startedMutationId = await derivedMutationId(batchMutationId, "started");
7499
+ const completedMutationId = await derivedMutationId(batchMutationId, "completed");
7500
+ let state = await loadNetworkPersonRegistry(db);
7501
+ const completed = matchingEvent(state.events, completedMutationId, "bulk_import_completed");
7502
+ if (completed) {
7503
+ if (eventString(completed, "batchDigest") !== batchDigest) {
7504
+ throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id was already used for different rows");
7505
+ }
7506
+ return {
7507
+ version: NETWORK_PERSON_BULK_IMPORT_VERSION,
7508
+ batchDigest,
7509
+ stateDigest: eventString(completed, "stateDigest"),
7510
+ rowCount: Number(completed.detail.rowCount) || rows5.length,
7511
+ counts: eventCounts(completed),
7512
+ rows: [],
7513
+ replayed: true
7514
+ };
7515
+ }
7516
+ let started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
7517
+ if (!started) {
7518
+ const preview = await previewNetworkPersonBulkImport(db, rows5, allowedChapterIds);
7519
+ if (preview.stateDigest !== input.expectedStateDigest) {
7520
+ throw new NetworkPersonRegistryError("bulk_state_changed", "network Person registry changed after preview; preview again");
7521
+ }
7522
+ try {
7523
+ await db.transact([
7524
+ networkPersonEventOp("bulk_import_started", startedMutationId, actor, now, {
7525
+ batchDigest,
7526
+ stateDigest: input.expectedStateDigest,
7527
+ rowCount: rows5.length,
7528
+ previewCounts: preview.counts
7529
+ })
7530
+ ], {
7531
+ mutationId: startedMutationId,
7532
+ guards: [{ ns: "networkPersonEvents", id: `network_person_event:${startedMutationId}`, exists: false }]
7533
+ });
7534
+ } catch (error) {
7535
+ if (!(error instanceof Error) || error.code !== "transact_guard_failed") throw error;
7536
+ }
7537
+ state = await loadNetworkPersonRegistry(db);
7538
+ started = matchingEvent(state.events, startedMutationId, "bulk_import_started");
7539
+ }
7540
+ if (!started || eventString(started, "batchDigest") !== batchDigest || eventString(started, "stateDigest") !== input.expectedStateDigest) {
7541
+ throw new NetworkPersonRegistryError("bulk_mutation_conflict", "batch mutation id does not match the reviewed preview");
7542
+ }
7543
+ const classified = await classifyNetworkPersonBulkRows(rows5, allowedChapterIds, state);
7544
+ const results = [];
7545
+ const counts = networkPersonBulkEmptyCounts();
7546
+ for (let index2 = 0; index2 < classified.rows.length; index2 += 1) {
7547
+ const row = classified.rows[index2];
7548
+ const value = classified.values[index2];
7549
+ if (!value) {
7550
+ results.push(row);
7551
+ incrementNetworkPersonBulkCount(counts, row.status);
7552
+ continue;
7553
+ }
7554
+ try {
7555
+ const result = await importNetworkPerson(
7556
+ db,
7557
+ value,
7558
+ await derivedMutationId(batchMutationId, `row_${index2 + 2}`),
7559
+ actor,
7560
+ now + index2 + 1
7561
+ );
7562
+ const status = "replayed" in result && result.replayed ? "replay" : result.status === "ambiguous" ? "exact_match_review" : "new";
7563
+ const applied = {
7564
+ rowNumber: index2 + 2,
7565
+ status,
7566
+ ...result.status === "ambiguous" ? { candidateCount: result.alias.candidatePersonIds?.length ?? 0 } : {}
7567
+ };
7568
+ results.push(applied);
7569
+ incrementNetworkPersonBulkCount(counts, status);
7570
+ } catch (error) {
7571
+ const failed = {
7572
+ rowNumber: index2 + 2,
7573
+ status: "failed",
7574
+ errors: [error instanceof Error ? error.message : "row failed"]
7575
+ };
7576
+ results.push(failed);
7577
+ incrementNetworkPersonBulkCount(counts, failed.status);
7578
+ }
7579
+ }
7580
+ if (counts.failed === 0) {
7581
+ await db.transact([
7582
+ networkPersonEventOp("bulk_import_completed", completedMutationId, actor, now + rows5.length + 1, {
7583
+ batchDigest,
7584
+ stateDigest: input.expectedStateDigest,
7585
+ rowCount: rows5.length,
7586
+ counts
7587
+ })
7588
+ ], { mutationId: completedMutationId });
7589
+ }
7590
+ return {
7591
+ version: NETWORK_PERSON_BULK_IMPORT_VERSION,
7592
+ batchDigest,
7593
+ stateDigest: input.expectedStateDigest,
7594
+ rowCount: rows5.length,
7595
+ counts,
7596
+ rows: results
7597
+ };
7598
+ }
7599
+
7335
7600
  // src/network-person-route-projection.ts
7336
7601
  function redactedName(displayName) {
7337
7602
  return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
@@ -7448,7 +7713,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
7448
7713
 
7449
7714
  // src/worker-routes-network-person-registry.ts
7450
7715
  var PATH2 = "/api/admin/network-person-registry";
7451
- var MAX_BODY2 = 32e3;
7716
+ var MAX_BODY2 = 8 * 1024 * 1024;
7452
7717
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
7453
7718
  var positiveRevision = (value) => {
7454
7719
  const parsed = Number(value);
@@ -7463,7 +7728,7 @@ async function gate10(req, env, ctx) {
7463
7728
  }
7464
7729
  function registryError(error) {
7465
7730
  if (error instanceof NetworkPersonRegistryError) {
7466
- const status = error.code.includes("stale") || error.code === "identity_conflict" ? 409 : error.code.endsWith("not_found") ? 404 : 400;
7731
+ 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
7732
  return json({ error: error.message, code: error.code }, status);
7468
7733
  }
7469
7734
  if (error instanceof Error && error.code === "transact_guard_failed") {
@@ -7498,6 +7763,23 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
7498
7763
  if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
7499
7764
  const actionActor = { id: got.user.userId };
7500
7765
  try {
7766
+ if (command === "preview-bulk-import") {
7767
+ const result = await previewNetworkPersonBulkImport(
7768
+ db,
7769
+ input.rows,
7770
+ ctx.chapter.networkPersonRegistry.chapterIds
7771
+ );
7772
+ return json({ result });
7773
+ }
7774
+ if (command === "apply-bulk-import") {
7775
+ const result = await applyNetworkPersonBulkImport(db, {
7776
+ rows: input.rows,
7777
+ expectedBatchDigest: typeof input.expectedBatchDigest === "string" ? input.expectedBatchDigest : "",
7778
+ expectedStateDigest: typeof input.expectedStateDigest === "string" ? input.expectedStateDigest : "",
7779
+ reviewed: input.reviewed === true
7780
+ }, mutationId, actionActor, ctx.chapter.networkPersonRegistry.chapterIds);
7781
+ return json({ result }, result.replayed ? 200 : 201);
7782
+ }
7501
7783
  if (command === "import") {
7502
7784
  const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
7503
7785
  if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);