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