@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.
@@ -11,6 +11,8 @@ interface ChapterTierConfig {
11
11
  id: string;
12
12
  name: string;
13
13
  priceCents: number;
14
+ currency?: string;
15
+ interval?: "year" | "month";
14
16
  stripePriceId?: string;
15
17
  blurb?: string;
16
18
  /** Defaults to declaration order when omitted. */
@@ -946,6 +948,10 @@ interface SignupControlTier {
946
948
  available: boolean;
947
949
  sortOrder: number;
948
950
  priceCents: number;
951
+ /** ISO currency; omitted only by pre-field schema-v1 documents. */
952
+ currency?: string;
953
+ /** Recurring interval; omitted only by pre-field schema-v1 documents. */
954
+ interval?: "year" | "month";
949
955
  stripePriceId?: string;
950
956
  blurb?: string;
951
957
  }
@@ -11,6 +11,8 @@ interface ChapterTierConfig {
11
11
  id: string;
12
12
  name: string;
13
13
  priceCents: number;
14
+ currency?: string;
15
+ interval?: "year" | "month";
14
16
  stripePriceId?: string;
15
17
  blurb?: string;
16
18
  /** Defaults to declaration order when omitted. */
@@ -946,6 +948,10 @@ interface SignupControlTier {
946
948
  available: boolean;
947
949
  sortOrder: number;
948
950
  priceCents: number;
951
+ /** ISO currency; omitted only by pre-field schema-v1 documents. */
952
+ currency?: string;
953
+ /** Recurring interval; omitted only by pre-field schema-v1 documents. */
954
+ interval?: "year" | "month";
949
955
  stripePriceId?: string;
950
956
  blurb?: string;
951
957
  }
@@ -316,6 +316,8 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
316
316
  name: group.name,
317
317
  standardPriceCents: group.standardPriceCents ?? 0,
318
318
  foundingDiscountCents: group.foundingDiscountCents ?? 0,
319
+ currency: group.currency ?? "usd",
320
+ interval: group.interval ?? "year",
319
321
  tiers: [...tiers],
320
322
  // The browser evaluates the same conditions the server enforces.
321
323
  conditions,
@@ -348,6 +350,8 @@ function resolveTiers(group, rows5 = []) {
348
350
  groupId: String(group.id ?? ""),
349
351
  name: "Membership",
350
352
  priceCents: group.standardPriceCents ?? 0,
353
+ currency: group.currency ?? "usd",
354
+ interval: group.interval ?? "year",
351
355
  ...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
352
356
  sortOrder: 0,
353
357
  active: true
@@ -360,11 +364,13 @@ function findTier(tiers, tierId) {
360
364
  if (!tierId) return tiers.length === 1 ? tiers[0] : null;
361
365
  return tiers.find((tier) => tier.id === tierId) ?? null;
362
366
  }
363
- function joinConfigTiers(tiers) {
367
+ function joinConfigTiers(tiers, defaults = {}) {
364
368
  return tiers.map((tier) => ({
365
369
  id: tier.id,
366
370
  name: tier.name,
367
371
  priceCents: tier.priceCents,
372
+ currency: tier.currency ?? defaults.currency ?? "usd",
373
+ interval: tier.interval ?? defaults.interval ?? "year",
368
374
  blurb: tier.blurb ?? "",
369
375
  free: tierIsFree(tier)
370
376
  }));
@@ -377,6 +383,8 @@ function rowToTier(row, groupId) {
377
383
  groupId: String(row.groupId ?? groupId),
378
384
  name: String(row.name ?? ""),
379
385
  priceCents: typeof row.priceCents === "number" ? row.priceCents : 0,
386
+ ...typeof row.currency === "string" && row.currency ? { currency: row.currency } : {},
387
+ ...row.interval === "year" || row.interval === "month" ? { interval: row.interval } : {},
380
388
  ...typeof row.stripePriceId === "string" && row.stripePriceId ? { stripePriceId: row.stripePriceId } : {},
381
389
  ...typeof row.blurb === "string" ? { blurb: row.blurb } : {},
382
390
  sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
@@ -1491,6 +1499,8 @@ function groupAttrs(document) {
1491
1499
  const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1492
1500
  return {
1493
1501
  ...first4 ? { standardPriceCents: first4.priceCents } : {},
1502
+ currency: first4?.currency ?? "usd",
1503
+ interval: first4?.interval ?? "year",
1494
1504
  foundingDiscountCents: 0,
1495
1505
  ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1496
1506
  ...document.policy ?? {},
@@ -1524,6 +1534,8 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
1524
1534
  groupId: document.target.chapterId,
1525
1535
  name: tier.name,
1526
1536
  priceCents: tier.priceCents,
1537
+ currency: tier.currency ?? "usd",
1538
+ interval: tier.interval ?? "year",
1527
1539
  ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
1528
1540
  ...tier.blurb ? { blurb: tier.blurb } : {},
1529
1541
  sortOrder: tier.sortOrder,
@@ -1940,7 +1952,7 @@ var handleMember = async (req, url, env, ctx) => {
1940
1952
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
1941
1953
  const application = controlledApplication(chapter.application, activeControl?.document);
1942
1954
  return json({
1943
- ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1955
+ ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
1944
1956
  fieldContract: chapterFlowFieldContract(chapter, "join", application),
1945
1957
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1946
1958
  ...activeControl ? { signupControl: {
@@ -2297,7 +2309,7 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2297
2309
  "theme",
2298
2310
  "giftOffers"
2299
2311
  ]);
2300
- var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2312
+ var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "currency", "interval", "stripePriceId", "blurb"]);
2301
2313
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2302
2314
  function unknownKeys(value, allowed2, path, errors) {
2303
2315
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
@@ -2341,6 +2353,12 @@ function tiersOf(value, errors) {
2341
2353
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2342
2354
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2343
2355
  }
2356
+ if (item.currency !== void 0 && (typeof item.currency !== "string" || !/^[a-z]{3}$/.test(item.currency))) {
2357
+ errors.push(`${path}.currency must be a lowercase ISO currency code`);
2358
+ }
2359
+ if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
2360
+ errors.push(`${path}.interval must be year or month`);
2361
+ }
2344
2362
  const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2345
2363
  return [{
2346
2364
  id,
@@ -2348,6 +2366,8 @@ function tiersOf(value, errors) {
2348
2366
  available: item.available === true,
2349
2367
  sortOrder: Number(item.sortOrder) || 0,
2350
2368
  priceCents: Number(item.priceCents) || 0,
2369
+ ...typeof item.currency === "string" ? { currency: item.currency } : {},
2370
+ ...item.interval === "year" || item.interval === "month" ? { interval: item.interval } : {},
2351
2371
  ...typeof item.stripePriceId === "string" ? { stripePriceId: item.stripePriceId } : {},
2352
2372
  ...blurb ? { blurb } : {}
2353
2373
  }];
@@ -6874,6 +6894,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
6874
6894
  // src/network-person-registry.ts
6875
6895
  var TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
6876
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;
6877
6898
  function normalizeNetworkIdentity(kind, value) {
6878
6899
  const trimmed = value.trim();
6879
6900
  if (kind === "email") return trimmed.toLowerCase();
@@ -6900,6 +6921,9 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
6900
6921
  if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
6901
6922
  if (!provenance || provenance.length > 500) errors.push("provenance is required and bounded");
6902
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
+ }
6903
6927
  const rawConsent = raw2.consent && typeof raw2.consent === "object" && !Array.isArray(raw2.consent) ? raw2.consent : {};
6904
6928
  const consentStates = ["unknown", "granted", "denied"];
6905
6929
  const marketing = typeof rawConsent.marketing === "string" ? rawConsent.marketing : "";
@@ -7312,6 +7336,247 @@ async function suppressNetworkPerson(db, input, mutationId, actor, now = Date.no
7312
7336
  return next;
7313
7337
  }
7314
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
+
7315
7580
  // src/network-person-route-projection.ts
7316
7581
  function redactedName(displayName) {
7317
7582
  return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
@@ -7428,7 +7693,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
7428
7693
 
7429
7694
  // src/worker-routes-network-person-registry.ts
7430
7695
  var PATH2 = "/api/admin/network-person-registry";
7431
- var MAX_BODY2 = 32e3;
7696
+ var MAX_BODY2 = 8 * 1024 * 1024;
7432
7697
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
7433
7698
  var positiveRevision = (value) => {
7434
7699
  const parsed = Number(value);
@@ -7443,7 +7708,7 @@ async function gate10(req, env, ctx) {
7443
7708
  }
7444
7709
  function registryError(error) {
7445
7710
  if (error instanceof NetworkPersonRegistryError) {
7446
- 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;
7447
7712
  return json({ error: error.message, code: error.code }, status);
7448
7713
  }
7449
7714
  if (error instanceof Error && error.code === "transact_guard_failed") {
@@ -7478,6 +7743,23 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
7478
7743
  if (!MUTATION_ID4.test(mutationId)) return json({ error: "a valid mutationId is required" }, 400);
7479
7744
  const actionActor = { id: got.user.userId };
7480
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
+ }
7481
7763
  if (command === "import") {
7482
7764
  const validated = validateNetworkPersonImport(input, ctx.chapter.networkPersonRegistry.chapterIds);
7483
7765
  if (!validated.ok || !validated.value) return json({ error: "invalid import", details: validated.errors }, 400);