@devilsdev/rag-core 3.8.1 → 3.8.2

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/index.cjs CHANGED
@@ -344,6 +344,9 @@ var require_plugin_signature_verifier = __commonJS({
344
344
  }
345
345
  return DIGEST_PREFIX + hash.digest("hex");
346
346
  }
347
+ function hasContentDigest(manifest) {
348
+ return Boolean(manifest) && Object.hasOwn(manifest, "contentDigest") && manifest.contentDigest !== void 0 && manifest.contentDigest !== null;
349
+ }
347
350
  var PluginSignatureVerifier = class {
348
351
  constructor(options = {}) {
349
352
  this.enabled = options.enabled !== false;
@@ -412,37 +415,13 @@ var require_plugin_signature_verifier = __commonJS({
412
415
  }
413
416
  return verificationResult;
414
417
  }
415
- if (!manifest || typeof manifest !== "object") {
416
- throw new Error("Invalid manifest: must be an object");
417
- }
418
- if (!signature || typeof signature !== "string") {
419
- throw new Error("Invalid signature: must be a non-empty string");
420
- }
421
- if (!signerId || typeof signerId !== "string") {
422
- throw new Error("Invalid signerId: must be a non-empty string");
423
- }
424
- const trustedKey = this.trustStore.get(signerId);
425
- if (!trustedKey) {
426
- throw new Error(`Untrusted signer: ${signerId}`);
427
- }
428
- const canonicalManifest = this._canonicalizeManifest(manifest);
429
- const manifestData = Buffer.from(
430
- JSON.stringify(canonicalManifest),
431
- "utf8"
432
- );
433
- const signatureBuffer = Buffer.from(signature, "base64");
434
- const isValid = await this._verifyEd25519Signature(
435
- manifestData,
436
- signatureBuffer,
437
- trustedKey.publicKey
438
- );
439
- if (!isValid) {
440
- throw new Error("Signature verification failed");
441
- }
442
- verificationResult.contentVerified = this._verifyContentDigest(
443
- canonicalManifest,
418
+ const { canonicalManifest, contentVerified } = this.verifyManifestSync(
419
+ manifest,
420
+ signature,
421
+ signerId,
444
422
  content
445
423
  );
424
+ verificationResult.contentVerified = contentVerified;
446
425
  verificationResult.verified = true;
447
426
  verificationResult.auditTrail.push({
448
427
  action: "signature_verified",
@@ -470,6 +449,51 @@ var require_plugin_signature_verifier = __commonJS({
470
449
  this._auditVerification(verificationResult, manifest);
471
450
  return verificationResult;
472
451
  }
452
+ /**
453
+ * Synchronous signature check shared by every verification path: input
454
+ * validation, trusted-signer lookup, Ed25519 signature over the canonical
455
+ * manifest, and the contentDigest check. Does not consult `enabled` and
456
+ * does not write audit entries; callers decide both.
457
+ *
458
+ * @param {object} manifest - Plugin manifest
459
+ * @param {string} signature - Base64 encoded signature
460
+ * @param {string} signerId - Signer identifier
461
+ * @param {object} [content] - `{ files, baseDir, contents }` to check
462
+ * against manifest.contentDigest (see computeContentDigest)
463
+ * @returns {{contentVerified: boolean, canonicalManifest: object}}
464
+ * contentVerified is false when the manifest has no contentDigest
465
+ * @throws {Error} When the manifest must not be trusted
466
+ */
467
+ verifyManifestSync(manifest, signature, signerId, content = null) {
468
+ if (!manifest || typeof manifest !== "object") {
469
+ throw new Error("Invalid manifest: must be an object");
470
+ }
471
+ if (!signature || typeof signature !== "string") {
472
+ throw new Error("Invalid signature: must be a non-empty string");
473
+ }
474
+ if (!signerId || typeof signerId !== "string") {
475
+ throw new Error("Invalid signerId: must be a non-empty string");
476
+ }
477
+ const trustedKey = this.trustStore.get(signerId);
478
+ if (!trustedKey) {
479
+ throw new Error(`Untrusted signer: ${signerId}`);
480
+ }
481
+ const canonicalManifest = this._canonicalizeManifest(manifest);
482
+ const manifestData = Buffer.from(JSON.stringify(canonicalManifest), "utf8");
483
+ const signatureBuffer = Buffer.from(signature, "base64");
484
+ if (!this._checkEd25519Signature(
485
+ manifestData,
486
+ signatureBuffer,
487
+ trustedKey.publicKey
488
+ )) {
489
+ throw new Error("Signature verification failed");
490
+ }
491
+ const contentVerified = this._verifyContentDigest(
492
+ canonicalManifest,
493
+ content
494
+ );
495
+ return { contentVerified, canonicalManifest };
496
+ }
473
497
  /**
474
498
  * Check the supplied plugin content against the signed contentDigest.
475
499
  * @param {object} canonicalManifest - Signed manifest
@@ -478,9 +502,9 @@ var require_plugin_signature_verifier = __commonJS({
478
502
  * @private
479
503
  */
480
504
  _verifyContentDigest(canonicalManifest, content) {
481
- const declared = Object.hasOwn(canonicalManifest, "contentDigest") ? canonicalManifest.contentDigest : void 0;
505
+ const declared = hasContentDigest(canonicalManifest) ? canonicalManifest.contentDigest : void 0;
482
506
  const hasContent = content && (Array.isArray(content.files) && content.files.length > 0 || content.contents && Object.keys(content.contents).length > 0);
483
- if (declared === void 0 || declared === null) {
507
+ if (declared === void 0) {
484
508
  if (this.requireContentDigest) {
485
509
  throw new Error(
486
510
  "Manifest has no contentDigest; the signature does not cover the plugin code"
@@ -514,6 +538,17 @@ var require_plugin_signature_verifier = __commonJS({
514
538
  * @returns {Promise<boolean>} Verification result
515
539
  */
516
540
  async _verifyEd25519Signature(data, signature, publicKey) {
541
+ return this._checkEd25519Signature(data, signature, publicKey);
542
+ }
543
+ /**
544
+ * Synchronous form of _verifyEd25519Signature.
545
+ * @param {Buffer} data - Data that was signed
546
+ * @param {Buffer} signature - Signature bytes
547
+ * @param {crypto.KeyObject|Buffer|string} publicKey - Ed25519 public key
548
+ * @returns {boolean}
549
+ * @private
550
+ */
551
+ _checkEd25519Signature(data, signature, publicKey) {
517
552
  const keyObject = toEd25519PublicKey(publicKey);
518
553
  if (!Buffer.isBuffer(signature) || signature.length !== 64) {
519
554
  return false;
@@ -695,6 +730,7 @@ var require_plugin_signature_verifier = __commonJS({
695
730
  PluginSignatureVerifier,
696
731
  createPluginVerifier,
697
732
  computeContentDigest,
733
+ hasContentDigest,
698
734
  toEd25519PublicKey
699
735
  };
700
736
  }
@@ -1137,6 +1173,23 @@ ${errorMessage}`
1137
1173
  isSignatureVerificationEnabled() {
1138
1174
  return Boolean(this._signatureVerifier && this._signatureVerifier.enabled);
1139
1175
  }
1176
+ /**
1177
+ * The signature verifier this registry uses (trust store, policy flags).
1178
+ * @returns {object} The PluginSignatureVerifier
1179
+ */
1180
+ getSignatureVerifier() {
1181
+ return this._signatureVerifier;
1182
+ }
1183
+ /**
1184
+ * Record an audit entry for a plugin verification decision made outside the
1185
+ * registry (for example when a configured plugin is checked before load).
1186
+ * @param {string} action - Audit action
1187
+ * @param {object} details - Audit details
1188
+ * @returns {void}
1189
+ */
1190
+ recordAuditEntry(action, details) {
1191
+ this._emitAuditEntry(action, details);
1192
+ }
1140
1193
  /**
1141
1194
  * Whether plugins without a signed manifest are rejected.
1142
1195
  * @returns {boolean}
@@ -1384,7 +1437,7 @@ var require_package = __commonJS({
1384
1437
  "package.json"(exports2, module2) {
1385
1438
  module2.exports = {
1386
1439
  name: "@devilsdev/rag-pipeline-utils",
1387
- version: "3.8.1",
1440
+ version: "3.8.2",
1388
1441
  description: "Composable retrieval-augmented generation (RAG) for Node.js: grounded citations, evaluation, guardrails, hybrid retrieval and a CLI.",
1389
1442
  main: "dist/index.cjs",
1390
1443
  type: "commonjs",
@@ -1643,10 +1696,10 @@ var require_package = __commonJS({
1643
1696
  "why-is-node-running": "^3.2.2"
1644
1697
  },
1645
1698
  dependencies: {
1646
- "@devilsdev/rag-connectors": "3.8.1",
1647
- "@devilsdev/rag-core": "3.8.1",
1648
- "@devilsdev/rag-eval": "3.8.1",
1649
- "@devilsdev/rag-guardrails": "3.8.1",
1699
+ "@devilsdev/rag-connectors": "3.8.2",
1700
+ "@devilsdev/rag-core": "3.8.2",
1701
+ "@devilsdev/rag-eval": "3.8.2",
1702
+ "@devilsdev/rag-guardrails": "3.8.2",
1650
1703
  ajv: "^8.17.1",
1651
1704
  axios: "^1.21.0",
1652
1705
  chokidar: "^4.0.3",
@@ -2917,18 +2970,21 @@ var require_scoring = __commonJS({
2917
2970
  `\\b(${REFERENCE_WORDS})\\.?\\s+\\d+[a-z]?\\b`,
2918
2971
  "giu"
2919
2972
  );
2920
- var IDENTIFIER_RE = new RegExp("(\\p{L}[\\p{L}\\p{N}]*)-(\\d+)\\b", "gu");
2973
+ var IDENTIFIER_RE = new RegExp("(?<![\\p{L}\\p{N}])(\\p{N}*)(\\p{L}[\\p{L}\\p{N}]*)-(\\d+)\\b", "gu");
2921
2974
  function identifiers(text) {
2922
2975
  if (!text || typeof text !== "string") return [];
2923
2976
  return [
2924
2977
  ...new Set(
2925
- [...text.matchAll(IDENTIFIER_RE)].map((m) => `id:${m[0].toLowerCase()}`)
2978
+ [...text.matchAll(IDENTIFIER_RE)].map(
2979
+ // m[1] is digits before the word ("2kinesin-8"), not part of it
2980
+ (m) => `id:${m[0].slice(m[1].length).toLowerCase()}`
2981
+ )
2926
2982
  )
2927
2983
  ];
2928
2984
  }
2929
2985
  function withoutReferences(text) {
2930
2986
  if (!text || typeof text !== "string") return text;
2931
- return text.replace(REFERENCE_RE, " ").replace(IDENTIFIER_RE, (_, word) => word);
2987
+ return text.replace(REFERENCE_RE, " ").replace(IDENTIFIER_RE, (_, lead, word) => lead + word);
2932
2988
  }
2933
2989
  var MONTHS = [
2934
2990
  ["january", "jan"],
@@ -5245,13 +5301,82 @@ var require_chunking_engine = __commonJS({
5245
5301
  var require_access_fields = __commonJS({
5246
5302
  "src/retrieval/access-fields.js"(exports2, module2) {
5247
5303
  "use strict";
5304
+ var ACCESS_KEYS = ["requiredRole", "acl"];
5305
+ var STORED_KEY = "rag_access";
5306
+ var NO_ACCESS = "rag:no-access";
5248
5307
  function accessFields(doc) {
5249
5308
  const fields = {};
5250
5309
  if (doc.acl !== void 0) fields.acl = doc.acl;
5251
5310
  if (doc.requiredRole !== void 0) fields.requiredRole = doc.requiredRole;
5252
5311
  return fields;
5253
5312
  }
5254
- module2.exports = { accessFields };
5313
+ function storedMetadata(doc) {
5314
+ const metadata = doc.metadata || {};
5315
+ const access = accessFields(doc);
5316
+ return Object.keys(access).length ? { ...metadata, [STORED_KEY]: access } : metadata;
5317
+ }
5318
+ function readStoredMetadata(stored) {
5319
+ if (!stored || typeof stored !== "object" || !(STORED_KEY in stored)) {
5320
+ return { metadata: stored || {}, access: {} };
5321
+ }
5322
+ const { [STORED_KEY]: access, ...metadata } = stored;
5323
+ const readable = access && typeof access === "object" && !Array.isArray(access);
5324
+ return {
5325
+ metadata,
5326
+ access: readable ? accessFields(access) : { acl: [NO_ACCESS] }
5327
+ };
5328
+ }
5329
+ function restrictionLists(result) {
5330
+ const lists = [];
5331
+ if (!result || typeof result !== "object") return lists;
5332
+ for (const source of [result, result.metadata, result.doc]) {
5333
+ if (!source || typeof source !== "object") continue;
5334
+ for (const key of ACCESS_KEYS) {
5335
+ const value = source[key];
5336
+ if (value === void 0 || value === null || value === "") continue;
5337
+ const roles = [].concat(value);
5338
+ if (roles.length > 0) lists.push(roles);
5339
+ }
5340
+ }
5341
+ return lists;
5342
+ }
5343
+ var sameRoles = (a, b) => a.length === b.length && a.every((role) => b.includes(role));
5344
+ function mergeAccess(first, others) {
5345
+ const have = restrictionLists(first);
5346
+ const extra = [];
5347
+ for (const copy of others) {
5348
+ for (const roles of restrictionLists(copy)) {
5349
+ if (![...have, ...extra].some((list) => sameRoles(list, roles))) {
5350
+ extra.push(roles);
5351
+ }
5352
+ }
5353
+ }
5354
+ if (extra.length === 0) return first;
5355
+ const merged = { ...first };
5356
+ const free = ACCESS_KEYS.filter(
5357
+ (key) => restrictionLists({ [key]: merged[key] }).length === 0
5358
+ );
5359
+ if (extra.length <= free.length) {
5360
+ extra.forEach((roles, i) => {
5361
+ merged[free[i]] = roles;
5362
+ });
5363
+ return merged;
5364
+ }
5365
+ const lists = [...restrictionLists(accessFields(merged)), ...extra];
5366
+ const shared = lists[0].filter(
5367
+ (role) => lists.every((list) => list.includes(role))
5368
+ );
5369
+ delete merged.requiredRole;
5370
+ merged.acl = shared.length ? shared : [NO_ACCESS];
5371
+ return merged;
5372
+ }
5373
+ module2.exports = {
5374
+ NO_ACCESS,
5375
+ accessFields,
5376
+ storedMetadata,
5377
+ readStoredMetadata,
5378
+ mergeAccess
5379
+ };
5255
5380
  }
5256
5381
  });
5257
5382
 
@@ -5392,6 +5517,7 @@ var require_bm25_search = __commonJS({
5392
5517
  var require_rank_fusion = __commonJS({
5393
5518
  "src/retrieval/rank-fusion.js"(exports2, module2) {
5394
5519
  "use strict";
5520
+ var { mergeAccess } = require_access_fields();
5395
5521
  var DEFAULT_CONFIG = { k: 60 };
5396
5522
  function reciprocalRankFusion(resultSets, options = {}) {
5397
5523
  const { k = DEFAULT_CONFIG.k, weights } = options;
@@ -5415,15 +5541,17 @@ var require_rank_fusion = __commonJS({
5415
5541
  if (scoreMap.has(id)) {
5416
5542
  const entry = scoreMap.get(id);
5417
5543
  entry.score += rrfScore;
5544
+ entry.copies.push(doc);
5418
5545
  } else {
5419
5546
  const { score: _origScore, ...rest } = doc;
5420
- scoreMap.set(id, { score: rrfScore, fields: rest });
5547
+ scoreMap.set(id, { score: rrfScore, fields: rest, copies: [] });
5421
5548
  }
5422
5549
  }
5423
5550
  }
5424
5551
  const fused = [];
5425
5552
  for (const [id, entry] of scoreMap) {
5426
- fused.push({ ...entry.fields, id, score: entry.score });
5553
+ const fields = mergeAccess(entry.fields, entry.copies);
5554
+ fused.push({ ...fields, id, score: entry.score });
5427
5555
  }
5428
5556
  fused.sort((a, b) => b.score - a.score);
5429
5557
  return fused;
@@ -24,9 +24,11 @@ export function buildComponents(config: object, options?: {
24
24
  /**
25
25
  * Instantiate a custom plugin from `plugins.<stage>.<name>` in the config.
26
26
  * The module may export a class (constructed with the entry config), an
27
- * instance, or either of those as `default`.
27
+ * instance, or either of those as `default`. When the shared plugin registry
28
+ * has signature verification enabled (production by default), the module is
29
+ * verified against its signed manifest before it is loaded.
28
30
  */
29
- export function loadPluginModule(modulePath: any, entryConfig: any, configDir: any): any;
31
+ export function loadPluginModule(modulePath: any, entryConfig: any, configDir: any, name: any): any;
30
32
  export namespace BUILT_INS {
31
33
  namespace loader {
32
34
  function auto(): typeof import("../loader/document-reader");
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Loads plugins from validated _config path and registers them.
3
+ * @param {string} configPath
4
+ */
5
+ export function loadPluginsFromJson(configPath: string): Promise<void>;
6
+ /**
7
+ * Verify a plugin file against its signed manifest BEFORE it is imported
8
+ * (importing runs the plugin's code).
9
+ *
10
+ * The manifest is read from `<plugin file>.manifest.json`. Its
11
+ * `contentDigest` (if any) must cover the plugin file plus any extra files
12
+ * listed in `manifest.files` (paths relative to the plugin file's
13
+ * directory). Nothing is checked when the registry's signature verification
14
+ * is disabled; when it is enabled and the registry was created with
15
+ * `requireSignedPlugins: true`, a missing manifest is an error.
16
+ *
17
+ * @param {string} name - Plugin name
18
+ * @param {string} absPath - Absolute path of the plugin entry file
19
+ * @returns {Promise<void>}
20
+ */
21
+ export function verifyPluginBeforeImport(name: string, absPath: string): Promise<void>;
22
+ /**
23
+ * Synchronous form of verifyPluginBeforeImport for loaders that `require`
24
+ * the plugin. Same policy: nothing happens while the registry's signature
25
+ * verification is disabled; otherwise a plugin with a manifest must have a
26
+ * valid signature from a trusted signer and matching content, and a plugin
27
+ * without one loads with a one-time warning unless the registry requires
28
+ * signed plugins. Every check completes before any plugin code runs.
29
+ *
30
+ * @param {string} name - Plugin name
31
+ * @param {string} absPath - Absolute path of the file that will be loaded
32
+ * @returns {void}
33
+ * @throws {Error} When the plugin must not be loaded
34
+ */
35
+ export function verifyPluginBeforeLoad(name: string, absPath: string): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Validates the full .ragrc.json _config structure
3
+ * @param {object} config
4
+ * @returns {{ valid: boolean, errors?: any[] }}
5
+ */
6
+ export function validateRagrcSchema(config: object): {
7
+ valid: boolean;
8
+ errors?: any[];
9
+ };
10
+ /**
11
+ * Validates a plugin-only structure (for runtime plugin loading)
12
+ * @param {object} config
13
+ * @returns {{ valid: boolean, errors?: any[] }}
14
+ */
15
+ export function validatePluginSchema(config: object): {
16
+ valid: boolean;
17
+ errors?: any[];
18
+ };
@@ -113,6 +113,19 @@ declare class PluginRegistry {
113
113
  * @returns {boolean}
114
114
  */
115
115
  isSignatureVerificationEnabled(): boolean;
116
+ /**
117
+ * The signature verifier this registry uses (trust store, policy flags).
118
+ * @returns {object} The PluginSignatureVerifier
119
+ */
120
+ getSignatureVerifier(): object;
121
+ /**
122
+ * Record an audit entry for a plugin verification decision made outside the
123
+ * registry (for example when a configured plugin is checked before load).
124
+ * @param {string} action - Audit action
125
+ * @param {object} details - Audit details
126
+ * @returns {void}
127
+ */
128
+ recordAuditEntry(action: string, details: object): void;
116
129
  /**
117
130
  * Whether plugins without a signed manifest are rejected.
118
131
  * @returns {boolean}
@@ -12,6 +12,18 @@ export function probeContract(modulePath: string, stage: string, options?: {
12
12
  methods: string[];
13
13
  error?: string;
14
14
  }>;
15
+ /**
16
+ * Environment for the probe process: only what Node needs to start, so the
17
+ * plugin code being loaded does not see the caller's other variables. HOME,
18
+ * NODE_OPTIONS and NODE_PATH are left out on purpose.
19
+ *
20
+ * @param {string} [platform] - Defaults to process.platform
21
+ * @param {object} [source] - Defaults to process.env
22
+ * @returns {Object<string, string>}
23
+ */
24
+ export function probeEnvironment(platform?: string, source?: object): {
25
+ [x: string]: string;
26
+ };
15
27
  export namespace STAGE_METHODS {
16
28
  let loader: string[];
17
29
  let embedder: string[];
@@ -1,12 +1,20 @@
1
1
  /**
2
+ * Besides files with a source extension, the package entry file and any
3
+ * file a scanned file loads by relative path are scanned whatever their
4
+ * extension, because Node runs them as JavaScript.
5
+ *
2
6
  * @param {string} pluginPath - Plugin directory or single .js file
7
+ * @param {object} [options]
8
+ * @param {string} [options.entry] - Package entry file (absolute or relative to pluginPath)
3
9
  * @returns {{
4
10
  * findings: Array<{severity: string, rule: string, message: string, file: string, line?: number}>,
5
11
  * counts: {high: number, medium: number, low: number},
6
12
  * filesScanned: number
7
13
  * }}
8
14
  */
9
- export function scanPlugin(pluginPath: string): {
15
+ export function scanPlugin(pluginPath: string, options?: {
16
+ entry?: string;
17
+ }): {
10
18
  findings: Array<{
11
19
  severity: string;
12
20
  rule: string;
@@ -65,8 +65,11 @@ export class AuditLogger extends EventEmitter {
65
65
  };
66
66
  logBuffer: any[];
67
67
  encryptionKey: string | Buffer<ArrayBufferLike>;
68
- signingKey: any;
69
- sequenceNumber: number;
68
+ signingKey: string | Buffer<ArrayBufferLike>;
69
+ _chainState: any;
70
+ _chainLoad: any;
71
+ _chainDirty: boolean;
72
+ _flushQueue: Promise<void>;
70
73
  /**
71
74
  * Initialize audit logging system
72
75
  */
@@ -105,9 +108,20 @@ export class AuditLogger extends EventEmitter {
105
108
  */
106
109
  _writeAuditLog(auditEvent: any): Promise<any>;
107
110
  /**
108
- * Flush logs to persistent storage
111
+ * Flush logs to persistent storage. Flushes run one at a time, so records
112
+ * are numbered and chained in the order they are appended.
109
113
  */
110
114
  _flushLogs(): Promise<void>;
115
+ _flushOnce(): Promise<void>;
116
+ _endsMidLine(file: any): Promise<boolean>;
117
+ _stripChainFields(log: any): void;
118
+ /**
119
+ * Give each record its sequence, the link to the previous record, the key
120
+ * id, the integrity hash and (when signing is on) the signature, advancing
121
+ * `state` as it goes.
122
+ * @private
123
+ */
124
+ private _numberRecords;
111
125
  /**
112
126
  * Query audit logs
113
127
  */
@@ -115,13 +129,14 @@ export class AuditLogger extends EventEmitter {
115
129
  results: any[];
116
130
  total: number;
117
131
  hasMore: boolean;
132
+ skippedLines: number;
118
133
  }>;
119
134
  /**
120
135
  * Generate compliance report
121
136
  */
122
137
  generateComplianceReport(_options: any): Promise<{
123
138
  metadata: {
124
- _tenantId: any;
139
+ _tenantId: string;
125
140
  standard: any;
126
141
  period: {
127
142
  startDate: any;
@@ -148,7 +163,24 @@ export class AuditLogger extends EventEmitter {
148
163
  evidence: any[];
149
164
  }>;
150
165
  /**
151
- * Verify log integrity
166
+ * Verify log integrity.
167
+ *
168
+ * Besides the per-record checks it reads the chain: every record carries its
169
+ * sequence, a link to the previous record and a keyed hash over the whole
170
+ * record, and the chain file holds the latest state.
171
+ *
172
+ * - `integrityBreaks`: `{ file, line, sequence, reason }`, where reason is
173
+ * `edited`, `chain`, `order`, `tail`, `unparseable`, `chain file missing`,
174
+ * `chain file reset` or `chain file forged`.
175
+ * - `missingLogs`: gaps in the sequence, `{ after, before, count }`.
176
+ * - `unchained`: records written before chain links existed (counted, and
177
+ * they do not make `verified` false).
178
+ * - `otherKey`: records written with another signing key; they can't be
179
+ * checked, so `verified` is false.
180
+ * - `secondWriter`: duplicate or interleaved sequences that look like two
181
+ * writers on one log directory. A break is only reported here when every
182
+ * record involved verifies and every interleaved run links up on its own;
183
+ * otherwise it is reported as the tampering it may be.
152
184
  */
153
185
  verifyIntegrity(_options?: {}): Promise<{
154
186
  verified: boolean;
@@ -157,6 +189,15 @@ export class AuditLogger extends EventEmitter {
157
189
  tamperedLogs: any[];
158
190
  missingLogs: any[];
159
191
  integrityBreaks: any[];
192
+ unchained: {
193
+ count: number;
194
+ ranges: any[];
195
+ };
196
+ otherKey: {
197
+ count: number;
198
+ ranges: any[];
199
+ };
200
+ secondWriter: any[];
160
201
  }>;
161
202
  _calculateIntegrityHash(auditEvent: any): Promise<string>;
162
203
  _encryptSensitiveFields(auditEvent: any): Promise<any>;
@@ -190,9 +231,89 @@ export class AuditLogger extends EventEmitter {
190
231
  */
191
232
  _getCipherKey(): Buffer<ArrayBufferLike>;
192
233
  _createDecipher(encryptedField: any): crypto.DecipherGCM;
193
- _generateSigningKey(): Buffer<ArrayBufferLike>;
194
- _initializeIntegrityChain(): Promise<void>;
195
- _updateIntegrityChain(logs: any): Promise<void>;
234
+ /**
235
+ * Resolve the key behind the integrity hashes, chain links and signatures.
236
+ *
237
+ * Order: the `signingKey` option, then the RAG_AUDIT_SIGNING_KEY env var,
238
+ * then a key stored in `<logDir>/integrity/signing.key` (created on first
239
+ * use), and only if that file can't be used a random per-process key. A
240
+ * stored key lets the logs be checked after a restart, but anyone who can
241
+ * read the log folder can read it, so it only detects accidental damage and
242
+ * casual edits. Both of the last two warn once per process.
243
+ *
244
+ * @param {Object} _options - Constructor options
245
+ * @returns {Buffer|string} Key material
246
+ * @private
247
+ */
248
+ private _resolveSigningKey;
249
+ /**
250
+ * The key stored in `<logDir>/integrity/signing.key` as 128 hex digits and
251
+ * a newline, created if it doesn't exist yet. It is created with the 'wx'
252
+ * flag, so of several processes starting together exactly one writes it and
253
+ * the rest read what it wrote (briefly retrying while it is still empty).
254
+ * Modes are owner-only (0o600 file, 0o700 directory); Windows ignores POSIX
255
+ * modes. A file that exists but is unreadable or invalid is never
256
+ * overwritten. It lives next to chain.json, not under daily/, so it is never
257
+ * read as a log file.
258
+ * @returns {{key: ?Buffer, file: string, reason?: string}}
259
+ * @private
260
+ */
261
+ private _storedSigningKey;
262
+ _hmac(text: any): string;
263
+ _keyId(): string;
264
+ _genesisLink(chainId: any): string;
265
+ _chainLink(chainPrev: any, integrity: any): string;
266
+ _stateTag(chainId: any, sequence: any, lastHash: any): string;
267
+ _chainFilePath(): string;
268
+ /**
269
+ * Read chain.json. Status is `missing`, `unreadable`, `legacy` (a file from
270
+ * before 3.8.2), `invalid` (wrong shape, or a state tag that doesn't match
271
+ * under this key), `otherKey` (written with another signing key) or `ok`.
272
+ * @private
273
+ */
274
+ private _readChainFile;
275
+ /**
276
+ * The chain state (chain id, last sequence and link), loaded once, at the
277
+ * first write.
278
+ * @private
279
+ */
280
+ private _loadChainState;
281
+ /**
282
+ * Work out where the chain stands before the first write of this process.
283
+ *
284
+ * - chain.json is valid and the newest records don't go past it: continue.
285
+ * - Records go past it and link from it (the process ended after the
286
+ * file was appended to but before chain.json was rewritten): advance the state and continue.
287
+ * - Otherwise (chain.json missing, replaced, not valid under this key, or
288
+ * records that don't link) a new chain segment starts. It is never
289
+ * silent: it is recorded in chain.json `resets`, logged as a warning and
290
+ * emitted as `chain_reset`, and the sequence carries on from the highest
291
+ * one in the files. A chain problem never blocks logging.
292
+ * @private
293
+ */
294
+ private _recoverChainState;
295
+ /** Chained records of the newest daily file that has any, in file order. */
296
+ _newestChainedRecords(files: any): Promise<any[]>;
297
+ /**
298
+ * Move `state` forward over `records` if they continue it exactly: this
299
+ * key, consecutive sequences, each linked to the one before and with a
300
+ * valid hash. Leaves `state` alone otherwise.
301
+ * @private
302
+ */
303
+ private _advanceThrough;
304
+ /**
305
+ * Start a chain: fresh when no chained records exist yet (keeping the
306
+ * values of a pre-3.8.2 chain.json under `legacy`), or a new segment that
307
+ * continues the sequence when they do.
308
+ * @private
309
+ */
310
+ private _startChainSegment;
311
+ /**
312
+ * Write chain.json with the latest state. If the write fails the state is
313
+ * kept and written on the next flush.
314
+ * @private
315
+ */
316
+ private _persistChainState;
196
317
  _signAuditEvent(auditEvent: any): Promise<string>;
197
318
  _verifySignature(auditEvent: any): Promise<boolean>;
198
319
  /**
@@ -203,14 +324,26 @@ export class AuditLogger extends EventEmitter {
203
324
  * @private
204
325
  */
205
326
  private _getLogFilesInRange;
206
- _readLogFile(filepath: any): Promise<any[]>;
327
+ /**
328
+ * Every non-blank line of a daily file (queryLogs() counts the unreadable ones as skippedLines) as `{ line, record }`, where `record`
329
+ * is null when the line isn't a JSON object (a half-written or damaged
330
+ * line). `line` counts from 1.
331
+ */
332
+ _readLogEntries(filepath: any): Promise<any[]>;
333
+ /**
334
+ * The tenant a query asks for, or undefined when it names none. A tenantId
335
+ * (or the underscore form `_tenantId`, read only when `tenantId` is absent)
336
+ * that is present must be a string: null, undefined and other types used to
337
+ * match every tenant.
338
+ * @private
339
+ */
340
+ private _resolveTenantFilter;
207
341
  _matchesQuery(log: any, query: any): boolean;
208
342
  _checkCompliance(log: any, standard: any): {
209
343
  requirement: any;
210
344
  violation: boolean;
211
345
  evidence: any;
212
346
  };
213
- _sendToWebhook(logs: any): Promise<void>;
214
347
  /**
215
348
  * Cleanup old logs based on retention policy
216
349
  */
@@ -21,6 +21,12 @@ export class GuardrailsPipeline extends EventEmitter {
21
21
  /**
22
22
  * Run the full guarded pipeline.
23
23
  *
24
+ * With `options.stream` and a post-generation guard, the token stream is
25
+ * buffered, checked and redacted as a whole, then released as an async
26
+ * iterable: one token chunk with the sanitized answer, then a done chunk
27
+ * carrying the redacted citations/evaluation and the `guardrails` summary.
28
+ * A strict-mode block yields only a done chunk with `success: false`.
29
+ *
24
30
  * @param {object} input - Pipeline input
25
31
  * @param {string} input.query - The user query
26
32
  * @param {Float32Array} [input.queryVector] - Optional precomputed query vector
@@ -34,6 +40,28 @@ export class GuardrailsPipeline extends EventEmitter {
34
40
  options?: object;
35
41
  context?: object;
36
42
  }): Promise<object>;
43
+ /**
44
+ * Whether the retrieval guard has a check that needs documents.
45
+ * @private
46
+ */
47
+ private _retrievalChecksActive;
48
+ /**
49
+ * Milliseconds left of the caller's options.timeout, or undefined.
50
+ * @private
51
+ */
52
+ private _remainingTimeout;
53
+ /** @private One warning per pipeline instance. */
54
+ private _warnRetrievalSkipped;
55
+ _warnedRetrievalSkipped: boolean;
56
+ /**
57
+ * Collect a token stream, run the post-generation guard on the whole
58
+ * answer and return an async iterable that yields the sanitized answer as
59
+ * one token chunk and then the done chunk. A blocked answer yields no
60
+ * tokens, only a done chunk with the error.
61
+ *
62
+ * @private
63
+ */
64
+ private _releaseStream;
37
65
  /**
38
66
  * Citations and evaluation details copy sentences from the raw answer, so
39
67
  * scrub every string in them with the same PII patterns as the answer.
@@ -1,3 +1,4 @@
1
+ export const NO_ACCESS: "rag:no-access";
1
2
  /**
2
3
  * The access fields (`acl`, `requiredRole`) set on a document's top level.
3
4
  * Stores keep them on each result, as well as whatever metadata holds, so
@@ -6,3 +7,32 @@
6
7
  * @returns {object}
7
8
  */
8
9
  export function accessFields(doc: object): object;
10
+ /**
11
+ * The metadata to store for a document: its metadata, plus its top-level
12
+ * access fields under a reserved key when it has any.
13
+ * @param {object} doc
14
+ * @returns {object}
15
+ */
16
+ export function storedMetadata(doc: object): object;
17
+ /**
18
+ * Splits stored metadata back into the document's metadata and its
19
+ * top-level access fields. Unreadable access fields restrict the document
20
+ * to no one.
21
+ * @param {object} stored
22
+ * @returns {{ metadata: object, access: object }}
23
+ */
24
+ export function readStoredMetadata(stored: object): {
25
+ metadata: object;
26
+ access: object;
27
+ };
28
+ /**
29
+ * One result for copies of the same document: the first copy's fields,
30
+ * restricted by every restriction any copy sets. Restrictions the first
31
+ * copy lacks go on its free top-level access fields; when they don't fit,
32
+ * its top-level restrictions and the new ones become a single list of the
33
+ * roles they all share, which is at least as strict.
34
+ * @param {object} first
35
+ * @param {object[]} others
36
+ * @returns {object}
37
+ */
38
+ export function mergeAccess(first: object, others: object[]): object;
@@ -25,6 +25,25 @@ export class PluginSignatureVerifier {
25
25
  * @returns {Promise<object>} Verification result
26
26
  */
27
27
  verifyPluginSignature(manifest: object, signature: string, signerId: string, content?: object): Promise<object>;
28
+ /**
29
+ * Synchronous signature check shared by every verification path: input
30
+ * validation, trusted-signer lookup, Ed25519 signature over the canonical
31
+ * manifest, and the contentDigest check. Does not consult `enabled` and
32
+ * does not write audit entries; callers decide both.
33
+ *
34
+ * @param {object} manifest - Plugin manifest
35
+ * @param {string} signature - Base64 encoded signature
36
+ * @param {string} signerId - Signer identifier
37
+ * @param {object} [content] - `{ files, baseDir, contents }` to check
38
+ * against manifest.contentDigest (see computeContentDigest)
39
+ * @returns {{contentVerified: boolean, canonicalManifest: object}}
40
+ * contentVerified is false when the manifest has no contentDigest
41
+ * @throws {Error} When the manifest must not be trusted
42
+ */
43
+ verifyManifestSync(manifest: object, signature: string, signerId: string, content?: object): {
44
+ contentVerified: boolean;
45
+ canonicalManifest: object;
46
+ };
28
47
  /**
29
48
  * Check the supplied plugin content against the signed contentDigest.
30
49
  * @param {object} canonicalManifest - Signed manifest
@@ -42,6 +61,15 @@ export class PluginSignatureVerifier {
42
61
  * @returns {Promise<boolean>} Verification result
43
62
  */
44
63
  _verifyEd25519Signature(data: Buffer, signature: Buffer, publicKey: crypto.KeyObject | Buffer | string): Promise<boolean>;
64
+ /**
65
+ * Synchronous form of _verifyEd25519Signature.
66
+ * @param {Buffer} data - Data that was signed
67
+ * @param {Buffer} signature - Signature bytes
68
+ * @param {crypto.KeyObject|Buffer|string} publicKey - Ed25519 public key
69
+ * @returns {boolean}
70
+ * @private
71
+ */
72
+ private _checkEd25519Signature;
45
73
  /**
46
74
  * Canonicalize manifest for consistent signing
47
75
  * @param {object} manifest - Plugin manifest
@@ -125,6 +153,14 @@ export function computeContentDigest({ files, baseDir, contents }?: {
125
153
  [x: string]: Buffer | string;
126
154
  };
127
155
  }): string;
156
+ /**
157
+ * Whether a manifest binds the plugin code: it has a contentDigest that is
158
+ * neither undefined nor null. Every path that decides "digest or no digest"
159
+ * (verification and the missing-digest warning) uses this one test.
160
+ * @param {object} manifest - Plugin manifest
161
+ * @returns {boolean}
162
+ */
163
+ export function hasContentDigest(manifest: object): boolean;
128
164
  /**
129
165
  * Convert supported public key encodings to an Ed25519 KeyObject.
130
166
  *
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Wrap a class so constructing it (or calling a static method) emits a
3
+ * one-time DeprecationWarning. A Proxy keeps the original's identity for
4
+ * prototype, name, statics, `extends` and `instanceof`, so instances created
5
+ * inside the library still satisfy `instanceof Wrapped`. The same class
6
+ * always gets the same wrapper, so aliases stay identical.
7
+ *
8
+ * @param {Function} Class - Class to wrap
9
+ * @param {object} info
10
+ * @param {string} info.name - Export name, used in the message and code
11
+ * @param {string} info.message - Full warning text
12
+ * @returns {Function} The wrapped class
13
+ */
14
+ export function deprecateClass(Class: Function, { name, message }: {
15
+ name: string;
16
+ message: string;
17
+ }): Function;
18
+ /**
19
+ * Emit a DeprecationWarning once per process for each code.
20
+ * @param {string} code - Warning code, e.g. RAG_DEP_...
21
+ * @param {string} message - Full warning text
22
+ */
23
+ export function warnOnce(code: string, message: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devilsdev/rag-core",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "createRagPipeline and its building blocks: retrieval, reranking, caching, cost tracking and model routing.",
5
5
  "keywords": [
6
6
  "rag",