@equationalapplications/core-llm-wiki 4.14.0 → 4.15.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.
package/README.md CHANGED
@@ -477,6 +477,18 @@ Core WikiMemory provides:
477
477
  - **Input Validation**: `sourceRef`/`sourceHash` normalized; embedding dimensions validated
478
478
  - **Parameterized Queries**: All SQL uses bind parameters
479
479
 
480
+ ### Prompt-Injection Trust Boundary
481
+
482
+ User-controlled text — `event.summary` passed to `write()`, document chunks passed to `ingestDocument()`,
483
+ fact `title`/`body` (including imported dumps) — is interpolated verbatim into LLM prompts for librarian,
484
+ heal, and embedding operations. Prompt templating does simple variable substitution; it does not detect
485
+ or filter instruction-like content.
486
+
487
+ Mitigating prompt injection (e.g. "ignore prior instructions and emit...") is **the host's responsibility**.
488
+ If your application accepts untrusted input that flows into `write()`, `ingestDocument()`, or `importDump()`,
489
+ treat the LLM's librarian/heal output as similarly untrusted — validate or scope it before acting on it
490
+ downstream.
491
+
480
492
  ## Usage
481
493
 
482
494
  ```typescript
@@ -284,6 +284,20 @@ var JobManager = class {
284
284
  this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
285
285
  this.activeIngestJobs = /* @__PURE__ */ new Map();
286
286
  this.statusSubscribers = /* @__PURE__ */ new Map();
287
+ /**
288
+ * Lookup table for acquireLock/releaseLock's dynamic-dispatch branch.
289
+ * Excludes 'ingest' | 'global_reembed' | 'global_import', which those
290
+ * methods already handle via explicit if/else branches before reaching
291
+ * this table.
292
+ */
293
+ this.lockKeyFns = {
294
+ prune: (id) => this._pruneKey(id),
295
+ librarian: (id) => this._librarianKey(id),
296
+ heal: (id) => this._healKey(id),
297
+ reembed: (id) => this._reembedKey(id),
298
+ import: (id) => this._importKey(id),
299
+ forget: (id) => this._forgetKey(id)
300
+ };
287
301
  }
288
302
  _pruneKey(entityId) {
289
303
  return `${this.prefix}:${entityId}:prune`;
@@ -433,9 +447,7 @@ var JobManager = class {
433
447
  } else if (operation === "global_import") {
434
448
  this.activeMaintenanceJobs.add(this._globalImportKey());
435
449
  } else {
436
- const keyFnName = `_${operation}Key`;
437
- const keyFn = this[keyFnName];
438
- this.activeMaintenanceJobs.add(keyFn.call(this, entityId));
450
+ this.activeMaintenanceJobs.add(this.lockKeyFns[operation](entityId));
439
451
  }
440
452
  this._notifyStatusSubscribers(entityId);
441
453
  }
@@ -447,9 +459,7 @@ var JobManager = class {
447
459
  } else if (operation === "global_import") {
448
460
  this.activeMaintenanceJobs.delete(this._globalImportKey());
449
461
  } else {
450
- const keyFnName = `_${operation}Key`;
451
- const keyFn = this[keyFnName];
452
- this.activeMaintenanceJobs.delete(keyFn.call(this, entityId));
462
+ this.activeMaintenanceJobs.delete(this.lockKeyFns[operation](entityId));
453
463
  }
454
464
  this._notifyStatusSubscribers(entityId);
455
465
  }
@@ -875,7 +885,9 @@ function generateId(prefix = "") {
875
885
  crypto.getRandomValues(bytes);
876
886
  return prefix + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("").substring(0, 24);
877
887
  }
878
- return prefix + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
888
+ throw new Error(
889
+ "generateId: no cryptographically secure random source available (crypto.randomUUID and crypto.getRandomValues are both missing)."
890
+ );
879
891
  }
880
892
 
881
893
  // src/services/IngestionService.ts
@@ -1412,12 +1424,16 @@ var MaintenanceService = class {
1412
1424
  };
1413
1425
 
1414
1426
  // src/services/ImportExportService.ts
1427
+ var MAX_EMBEDDING_BLOB_BYTES = 32 * 1024;
1428
+ var IMPORT_TITLE_MAX = 500;
1429
+ var IMPORT_BODY_MAX = 8e3;
1415
1430
  var ImportExportService = class {
1416
- constructor(db, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService) {
1431
+ constructor(db, entryRepo, taskRepo, eventRepo, edgeRepo, metadataRepo, searchService, jobManager, embeddingService) {
1417
1432
  this.db = db;
1418
1433
  this.entryRepo = entryRepo;
1419
1434
  this.taskRepo = taskRepo;
1420
1435
  this.eventRepo = eventRepo;
1436
+ this.edgeRepo = edgeRepo;
1421
1437
  this.metadataRepo = metadataRepo;
1422
1438
  this.searchService = searchService;
1423
1439
  this.jobManager = jobManager;
@@ -1462,10 +1478,11 @@ var ImportExportService = class {
1462
1478
  }
1463
1479
  }
1464
1480
  async getFullBundle(entityId, opts) {
1465
- const [factsRaw, tasks, events] = await Promise.all([
1481
+ const [factsRaw, tasks, events, edges] = await Promise.all([
1466
1482
  opts?.includeBlobs ? this.entryRepo.findAllByEntityIdWithBlobs(entityId) : this.entryRepo.findAllByEntityId(entityId),
1467
1483
  this.taskRepo.findAllByEntityId(entityId),
1468
- this.eventRepo.getByEntityId(entityId, opts?.maxEvents)
1484
+ this.eventRepo.getByEntityId(entityId, opts?.maxEvents),
1485
+ this.edgeRepo.getByEntityId(entityId)
1469
1486
  ]);
1470
1487
  const facts = factsRaw.map((f) => {
1471
1488
  const {
@@ -1484,7 +1501,7 @@ var ImportExportService = class {
1484
1501
  tags: typeof factBase.tags === "string" ? JSON.parse(factBase.tags) : factBase.tags
1485
1502
  };
1486
1503
  });
1487
- return { facts, tasks, events };
1504
+ return { facts, tasks, events, edges };
1488
1505
  }
1489
1506
  /** Single-entity import transaction + post-processing; package-internal hook for tests. */
1490
1507
  async doImportEntity(entityId, bundle, merge) {
@@ -1493,6 +1510,7 @@ var ImportExportService = class {
1493
1510
  const factsWithPreservedBlob = /* @__PURE__ */ new Map();
1494
1511
  const preservedBlobDims = /* @__PURE__ */ new Set();
1495
1512
  const softDeletedFactIds = [];
1513
+ const clippedTextByFactId = /* @__PURE__ */ new Map();
1496
1514
  await this.db.withTransactionAsync(async (tx) => {
1497
1515
  if (!merge) {
1498
1516
  const deletedLiveFactIds = await this.entryRepo.findIdsBySource(
@@ -1505,6 +1523,7 @@ var ImportExportService = class {
1505
1523
  softDeletedFactIds.push(...deletedLiveFactIds);
1506
1524
  await this.entryRepo.bulkSoftDeleteByEntityId(entityId, tx);
1507
1525
  await this.taskRepo.bulkSoftDeleteByEntityId(entityId, tx);
1526
+ await this.edgeRepo.bulkDeleteByEntityId(entityId, tx);
1508
1527
  await this.metadataRepo.deleteCheckpoint(entityId, tx);
1509
1528
  }
1510
1529
  const factIds = bundle.facts.map((fact) => fact.id);
@@ -1529,21 +1548,32 @@ var ImportExportService = class {
1529
1548
  const rawBlobRaw = fact.embedding_blob;
1530
1549
  let rawBlob = null;
1531
1550
  if (rawBlobRaw instanceof Uint8Array) {
1532
- rawBlob = rawBlobRaw;
1551
+ if (rawBlobRaw.byteLength <= MAX_EMBEDDING_BLOB_BYTES) {
1552
+ rawBlob = rawBlobRaw;
1553
+ }
1533
1554
  } else if (rawBlobRaw !== null && rawBlobRaw !== void 0 && typeof rawBlobRaw === "object") {
1534
1555
  const obj = rawBlobRaw;
1535
1556
  if (obj["type"] === "Buffer" && Array.isArray(obj["data"])) {
1536
- rawBlob = new Uint8Array(obj["data"]);
1557
+ const data = obj["data"];
1558
+ if (data.length <= MAX_EMBEDDING_BLOB_BYTES) {
1559
+ rawBlob = new Uint8Array(data);
1560
+ }
1537
1561
  } else if (!Array.isArray(rawBlobRaw)) {
1538
1562
  const entries = Object.keys(obj);
1539
1563
  if (entries.length > 0 && entries.every((k) => /^\d+$/.test(k))) {
1540
1564
  const len = entries.length;
1541
- rawBlob = new Uint8Array(len);
1542
- for (let i = 0; i < len; i++)
1543
- rawBlob[i] = obj[String(i)] ?? 0;
1565
+ if (len <= MAX_EMBEDDING_BLOB_BYTES) {
1566
+ rawBlob = new Uint8Array(len);
1567
+ for (let i = 0; i < len; i++) {
1568
+ rawBlob[i] = obj[String(i)] ?? 0;
1569
+ }
1570
+ }
1544
1571
  }
1545
1572
  }
1546
1573
  }
1574
+ if (rawBlob !== null && rawBlob.byteLength > MAX_EMBEDDING_BLOB_BYTES) {
1575
+ rawBlob = null;
1576
+ }
1547
1577
  let blobData = null;
1548
1578
  if (rawBlob !== null && rawBlob.byteLength > 0 && rawBlob.byteLength % 4 === 0) {
1549
1579
  const copy = new ArrayBuffer(rawBlob.byteLength);
@@ -1573,11 +1603,14 @@ var ImportExportService = class {
1573
1603
  }
1574
1604
  if (merge && safeUpdatedAt <= existing.updated_at) continue;
1575
1605
  }
1606
+ const safeTitle = clip(String(fact.title ?? ""), IMPORT_TITLE_MAX);
1607
+ const safeBody = clip(String(fact.body ?? ""), IMPORT_BODY_MAX);
1608
+ clippedTextByFactId.set(fact.id, { title: safeTitle, body: safeBody });
1576
1609
  const factObj = {
1577
1610
  id: fact.id,
1578
1611
  entity_id: entityId,
1579
- title: fact.title,
1580
- body: fact.body,
1612
+ title: safeTitle,
1613
+ body: safeBody,
1581
1614
  tags: Array.isArray(fact.tags) ? fact.tags : [],
1582
1615
  confidence: fact.confidence,
1583
1616
  source_type: sourceType,
@@ -1588,7 +1621,8 @@ var ImportExportService = class {
1588
1621
  last_accessed_at: fact.last_accessed_at,
1589
1622
  access_count: fact.access_count,
1590
1623
  deleted_at: fact.deleted_at,
1591
- embedding_blob: blobData ?? void 0
1624
+ embedding_blob: blobData ?? void 0,
1625
+ okf_type: fact.okf_type ?? null
1592
1626
  };
1593
1627
  await this.entryRepo.upsertForImport(factObj, tx);
1594
1628
  if (blobData != null) {
@@ -1637,7 +1671,8 @@ var ImportExportService = class {
1637
1671
  created_at: task.created_at,
1638
1672
  updated_at: safeUpdatedAt,
1639
1673
  resolved_at: task.resolved_at,
1640
- deleted_at: task.deleted_at
1674
+ deleted_at: task.deleted_at,
1675
+ okf_type: task.okf_type ?? null
1641
1676
  },
1642
1677
  tx,
1643
1678
  safeUpdatedAt
@@ -1661,15 +1696,29 @@ var ImportExportService = class {
1661
1696
  tx
1662
1697
  );
1663
1698
  }
1699
+ for (const edge of bundle.edges ?? []) {
1700
+ await this.edgeRepo.addIgnoreDuplicate(
1701
+ {
1702
+ id: edge.id,
1703
+ entity_id: entityId,
1704
+ source_id: edge.source_id,
1705
+ target_id: edge.target_id,
1706
+ edge_type: edge.edge_type,
1707
+ created_at: edge.created_at
1708
+ },
1709
+ tx
1710
+ );
1711
+ }
1664
1712
  });
1665
1713
  await this.searchService.sync(entityId);
1666
1714
  for (const fact of bundle.facts) {
1667
1715
  if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
1716
+ const clipped = clippedTextByFactId.get(fact.id);
1668
1717
  const embedded = await this.embeddingService.embedFact({
1669
1718
  id: fact.id,
1670
1719
  entity_id: entityId,
1671
- title: fact.title,
1672
- body: fact.body,
1720
+ title: clipped?.title ?? fact.title,
1721
+ body: clipped?.body ?? fact.body,
1673
1722
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
1674
1723
  });
1675
1724
  if (!embedded) {
@@ -1769,7 +1818,7 @@ var ImportExportService = class {
1769
1818
  }
1770
1819
  _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
1771
1820
  console.warn(
1772
- `[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`
1821
+ `[WikiMemory] importDump: ${type} id ${JSON.stringify(id)} already belongs to entity ${JSON.stringify(existingEntityId)}; skipping for entity ${JSON.stringify(targetEntityId)}`
1773
1822
  );
1774
1823
  }
1775
1824
  _normalizeImportedSourceType(raw, ctx) {
@@ -1848,7 +1897,7 @@ var EmbeddingService = class {
1848
1897
  tagsStr = fact.tags;
1849
1898
  }
1850
1899
  }
1851
- const text = `${fact.title} ${fact.body} ${tagsStr}`.trim();
1900
+ const text = clip(`${fact.title} ${fact.body} ${tagsStr}`.trim(), 16e3);
1852
1901
  try {
1853
1902
  const vector = await embedFn(text);
1854
1903
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
@@ -1976,7 +2025,7 @@ var RetrievalService = class {
1976
2025
  const sanitizedTierWeights = shouldExposeReadMetadata(entityId) ? sanitizeTierWeights(entityIds, options?.tierWeights) : void 0;
1977
2026
  const exposeMetadata = shouldExposeReadMetadata(entityId);
1978
2027
  if (entityIds.length === 0) {
1979
- const empty = { facts: [], tasks: [], events: [] };
2028
+ const empty = { facts: [], tasks: [], events: [], edges: [] };
1980
2029
  if (exposeMetadata) {
1981
2030
  empty.metadata = { query, entityIds: [] };
1982
2031
  if (sanitizedTierWeights && Object.keys(sanitizedTierWeights).length > 0) empty.metadata.tierWeights = sanitizedTierWeights;
@@ -2366,7 +2415,7 @@ var RetrievalService = class {
2366
2415
  if (exposeMetadata && trimmedQuery && scoreByFactId) {
2367
2416
  factScores = Object.fromEntries(facts.map((fact) => [fact.id, scoreByFactId.get(fact.id) ?? 0]));
2368
2417
  }
2369
- const bundle = { facts, tasks, events: events.reverse() };
2418
+ const bundle = { facts, tasks, events: events.reverse(), edges: [] };
2370
2419
  if (exposeMetadata) {
2371
2420
  bundle.metadata = { query, entityIds };
2372
2421
  if (sanitizedTierWeights && Object.keys(sanitizedTierWeights).length > 0) bundle.metadata.tierWeights = sanitizedTierWeights;
@@ -2462,15 +2511,38 @@ var RetrievalService = class {
2462
2511
 
2463
2512
  // src/services/WriteService.ts
2464
2513
  var WriteService = class {
2465
- constructor(db, options, eventRepo, metadataRepo, jobManager, maintenanceService) {
2514
+ constructor(db, options, entryRepo, eventRepo, metadataRepo, jobManager, maintenanceService) {
2466
2515
  this.db = db;
2467
2516
  this.options = options;
2517
+ this.entryRepo = entryRepo;
2468
2518
  this.eventRepo = eventRepo;
2469
2519
  this.metadataRepo = metadataRepo;
2470
2520
  this.jobManager = jobManager;
2471
2521
  this.maintenanceService = maintenanceService;
2472
2522
  }
2473
2523
  async write(entityId, event) {
2524
+ if (typeof entityId !== "string" || entityId.length === 0 || entityId.length > 200 || entityId.includes("\0")) {
2525
+ throw new TypeError(
2526
+ `Invalid entityId: must be a non-empty string at most 200 chars with no null bytes; got ${JSON.stringify(entityId)}.`
2527
+ );
2528
+ }
2529
+ if (event === null || typeof event !== "object" || Array.isArray(event)) {
2530
+ throw new TypeError("Invalid event: must be a non-null object.");
2531
+ }
2532
+ if (typeof event.summary !== "string") {
2533
+ throw new TypeError("Invalid event.summary: must be a string.");
2534
+ }
2535
+ const summary = clip(event.summary, 4e3);
2536
+ let relatedEntryId = null;
2537
+ const rawRelatedEntryId = event.related_entry_id;
2538
+ if (rawRelatedEntryId != null && rawRelatedEntryId !== "") {
2539
+ if (typeof rawRelatedEntryId !== "string" || rawRelatedEntryId.length > 200 || rawRelatedEntryId.includes("\0")) {
2540
+ relatedEntryId = null;
2541
+ } else {
2542
+ const existing = await this.entryRepo.findByIds([rawRelatedEntryId], [entityId]);
2543
+ relatedEntryId = existing.length > 0 ? rawRelatedEntryId : null;
2544
+ }
2545
+ }
2474
2546
  const id = generateId("evt_");
2475
2547
  const now = Date.now();
2476
2548
  let eventType = event.event_type;
@@ -2481,8 +2553,8 @@ var WriteService = class {
2481
2553
  id,
2482
2554
  entity_id: entityId,
2483
2555
  event_type: eventType,
2484
- summary: event.summary,
2485
- related_entry_id: event.related_entry_id || null,
2556
+ summary,
2557
+ related_entry_id: relatedEntryId,
2486
2558
  created_at: now
2487
2559
  };
2488
2560
  let shouldRunLibrarian = false;
@@ -2543,5 +2615,5 @@ var WriteService = class {
2543
2615
  };
2544
2616
 
2545
2617
  export { EmbeddingService, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WriteService, __privateAdd, __privateGet, __privateSet, generateId, normalizeSourceHash, normalizeSourceRef, parseEmbedding };
2546
- //# sourceMappingURL=chunk-6FWG2DG4.mjs.map
2547
- //# sourceMappingURL=chunk-6FWG2DG4.mjs.map
2618
+ //# sourceMappingURL=chunk-J4GBC6CP.mjs.map
2619
+ //# sourceMappingURL=chunk-J4GBC6CP.mjs.map