@devilsdev/rag-pipeline-utils 2.4.4 → 2.5.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.
Files changed (225) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +4992 -1242
  9. package/dist/index.d.ts +80 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  12. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  13. package/dist/types/agentic/query-planner.d.ts +145 -0
  14. package/dist/types/agentic/self-critique.d.ts +45 -0
  15. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  16. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  17. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  18. package/dist/types/ai/multimodal.d.ts +39 -0
  19. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  20. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  21. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  22. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  23. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  24. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  25. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  26. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  27. package/dist/types/citation/citation-tracker.d.ts +64 -0
  28. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  29. package/dist/types/citation/source-mapper.d.ts +37 -0
  30. package/dist/types/connectors/anthropic-connector.d.ts +92 -0
  31. package/dist/types/connectors/base-connector.d.ts +87 -0
  32. package/dist/types/connectors/cohere-connector.d.ts +89 -0
  33. package/dist/types/connectors/connector-utils.d.ts +110 -0
  34. package/dist/types/connectors/local-embedder.d.ts +86 -0
  35. package/dist/types/connectors/memory-retriever.d.ts +91 -0
  36. package/dist/types/connectors/ollama-connector.d.ts +110 -0
  37. package/dist/types/connectors/openai-connector.d.ts +114 -0
  38. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  39. package/dist/types/cost/cost-calculator.d.ts +152 -0
  40. package/dist/types/cost/cost-tracker.d.ts +126 -0
  41. package/dist/types/cost/token-budget.d.ts +110 -0
  42. package/dist/types/dag/core/error-context.d.ts +239 -0
  43. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  44. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  45. package/dist/types/dag/dag-engine.d.ts +156 -0
  46. package/dist/types/dag/dag-node.d.ts +37 -0
  47. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  48. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  49. package/dist/types/debug/execution-tracer.d.ts +57 -0
  50. package/dist/types/debug/trace-inspector.d.ts +31 -0
  51. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  52. package/dist/types/enterprise/data-governance.d.ts +203 -0
  53. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  54. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  55. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  56. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  57. package/dist/types/evaluate/groundedness.d.ts +60 -0
  58. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  59. package/dist/types/evaluate/relevance.d.ts +21 -0
  60. package/dist/types/evaluate/scoring.d.ts +123 -0
  61. package/dist/types/experimental.d.ts +15 -0
  62. package/dist/types/graph/entity-extractor.d.ts +84 -0
  63. package/dist/types/graph/graph-index.d.ts +49 -0
  64. package/dist/types/graph/graph-retriever.d.ts +81 -0
  65. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  66. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  67. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  68. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  69. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  70. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  71. package/dist/types/mcp/mcp-server.d.ts +85 -0
  72. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  73. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  74. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  75. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  76. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  77. package/dist/types/retrieval/file-vector-store.d.ts +87 -0
  78. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  79. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  80. package/dist/types/security/jwt-config.d.ts +80 -0
  81. package/dist/types/security/jwt-validator.d.ts +195 -0
  82. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  83. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  84. package/dist/types/security/validator.d.ts +5 -0
  85. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  86. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  87. package/dist/types/streaming/stream-router.d.ts +88 -0
  88. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  89. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  90. package/dist/types/templates/scaffold.d.ts +55 -0
  91. package/dist/types/templates/template-registry.d.ts +63 -0
  92. package/dist/types/utils/embed-query.d.ts +20 -0
  93. package/dist/types/utils/logger.d.ts +50 -0
  94. package/dist/types/utils/rate-limiter.d.ts +66 -0
  95. package/dist/types/utils/retry.d.ts +27 -0
  96. package/dist/types/utils/run-abort.d.ts +34 -0
  97. package/dist/types/utils/secure-logging.d.ts +51 -0
  98. package/dist/types/utils/structured-logger.d.ts +126 -0
  99. package/package.json +53 -29
  100. package/src/agentic/agentic-pipeline.js +75 -7
  101. package/src/agentic/iterative-retriever.js +47 -10
  102. package/src/agentic/query-planner.js +329 -78
  103. package/src/citation/citation-tracker.js +6 -4
  104. package/src/citation/hallucination-detector.js +2 -2
  105. package/src/citation/source-mapper.js +15 -28
  106. package/src/cli/commands/ai.js +101 -0
  107. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  108. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  109. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  110. package/src/cli/commands/plugin-hub.js +96 -36
  111. package/src/cli/diagnostics/config-checker.js +76 -63
  112. package/src/cli/diagnostics/environment-checker.js +62 -31
  113. package/src/cli/diagnostics/plugin-checker.js +36 -89
  114. package/src/cli/doctor-command.js +39 -5
  115. package/src/cli/enhanced-cli-commands.js +72 -217
  116. package/src/cli/handlers/ai-handler.js +364 -0
  117. package/src/cli/handlers/diagnostics-handler.js +78 -31
  118. package/src/cli/handlers/ingest-handler.js +28 -22
  119. package/src/cli/handlers/init-handler.js +148 -25
  120. package/src/cli/handlers/query-handler.js +41 -26
  121. package/src/cli/handlers/run-handler.js +16 -167
  122. package/src/cli/handlers/serve-handler.js +60 -0
  123. package/src/cli/services/bench-stats.js +54 -0
  124. package/src/cli/services/benchmark.js +161 -0
  125. package/src/cli/services/feedback.js +155 -0
  126. package/src/cli/services/rag-service.js +322 -0
  127. package/src/cli/shared/cli-utils.js +24 -7
  128. package/src/cli/shared/config-context.js +21 -0
  129. package/src/cli/wizard/config-builder.js +81 -0
  130. package/src/cli/wizard/init-wizard.js +617 -0
  131. package/src/config/component-factory.js +125 -0
  132. package/src/config/load-config.js +29 -10
  133. package/src/config/load-plugin-config.js +46 -2
  134. package/src/connectors/anthropic-connector.js +93 -55
  135. package/src/connectors/base-connector.js +74 -19
  136. package/src/connectors/cohere-connector.js +88 -32
  137. package/src/connectors/connector-utils.js +302 -0
  138. package/src/connectors/local-embedder.js +43 -5
  139. package/src/connectors/memory-retriever.js +108 -15
  140. package/src/connectors/ollama-connector.js +83 -49
  141. package/src/connectors/openai-connector.js +108 -44
  142. package/src/connectors/rag-prompt.js +88 -0
  143. package/src/core/create-pipeline.js +209 -60
  144. package/src/core/plugin-registry.js +87 -11
  145. package/src/cost/cost-calculator.js +205 -32
  146. package/src/dev/dev-server.js +166 -11
  147. package/src/dev/hot-reload.js +40 -1
  148. package/src/ecosystem/plugin-checks.js +181 -0
  149. package/src/ecosystem/plugin-hub.js +50 -125
  150. package/src/ecosystem/plugin-installer.js +298 -0
  151. package/src/ecosystem/plugin-probe.js +127 -0
  152. package/src/ecosystem/plugin-scanner.js +185 -0
  153. package/src/ecosystem/tar-extract.js +114 -0
  154. package/src/enterprise/audit-logging.js +158 -29
  155. package/src/enterprise/sso-integration.js +715 -228
  156. package/src/evaluate/context-metrics.js +28 -16
  157. package/src/evaluate/evaluator.js +3 -3
  158. package/src/evaluate/faithfulness.js +18 -14
  159. package/src/evaluate/groundedness.js +43 -12
  160. package/src/evaluate/pipeline-evaluator.js +35 -8
  161. package/src/evaluate/scoring.js +251 -3
  162. package/src/experimental.js +71 -0
  163. package/src/guardrails/guardrails-pipeline.js +69 -7
  164. package/src/guardrails/injection-detector.js +283 -0
  165. package/src/guardrails/post-generation-guard.js +19 -0
  166. package/src/guardrails/pre-retrieval-guard.js +36 -36
  167. package/src/guardrails/retrieval-guard.js +58 -8
  168. package/src/index.js +36 -1
  169. package/src/loader/document-reader.js +91 -0
  170. package/src/reranker/cascade-reranker.js +3 -0
  171. package/src/reranker/embedding-reranker.js +136 -21
  172. package/src/reranker/llm-reranker.js +79 -12
  173. package/src/reranker/scoring-reranker.js +3 -0
  174. package/src/retrieval/bm25-search.js +12 -6
  175. package/src/retrieval/file-vector-store.js +212 -0
  176. package/src/retrieval/hybrid-retriever.js +22 -19
  177. package/src/security/jwt-config.js +13 -2
  178. package/src/security/jwt-validator.js +134 -30
  179. package/src/security/plugin-sandbox.js +18 -1
  180. package/src/security/plugin-signature-verifier.js +245 -70
  181. package/src/utils/deprecate.js +69 -0
  182. package/src/utils/embed-query.js +33 -0
  183. package/src/utils/input-sanitizer.js +19 -4
  184. package/src/utils/logger.js +34 -4
  185. package/src/utils/run-abort.js +102 -0
  186. package/src/ai/federated-learning.js +0 -211
  187. package/src/ai/federation.js +0 -71
  188. package/src/ai/index.js +0 -22
  189. package/src/ai/model-trainer.js +0 -217
  190. package/src/ai/model-training.js +0 -202
  191. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  192. package/src/ai/orchestrator.js +0 -65
  193. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  194. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  195. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  196. package/src/cli/commands/ai-ml/index.js +0 -17
  197. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  198. package/src/cli/commands/ai-ml/shared.js +0 -51
  199. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  200. package/src/cli/commands/ai-ml.js +0 -386
  201. package/src/cli/commands/docs.js +0 -228
  202. package/src/cli/commands/dx.js +0 -538
  203. package/src/cli/commands/wizard.js +0 -627
  204. package/src/cli/input-sanitization-middleware.js +0 -356
  205. package/src/cli/interactive-wizard.js +0 -373
  206. package/src/cli/templates/development.template.json +0 -64
  207. package/src/cli/templates/minimal.template.json +0 -33
  208. package/src/cli/templates/production.template.json +0 -104
  209. package/src/cli/templates/testing.template.json +0 -45
  210. package/src/cli/wizard/configuration.js +0 -148
  211. package/src/cli/wizard/observability.js +0 -79
  212. package/src/cli/wizard/performance.js +0 -76
  213. package/src/cli/wizard/plugin-selection.js +0 -297
  214. package/src/cli/wizard/preview-save.js +0 -172
  215. package/src/cli/wizard/project-setup.js +0 -66
  216. package/src/core/pipeline-factory.js +0 -59
  217. package/src/dag/enhanced-dag-engine.js +0 -894
  218. package/src/dx/index.js +0 -40
  219. package/src/dx/integration-templates.js +0 -662
  220. package/src/dx/performance-profiler.js +0 -968
  221. package/src/dx/realtime-debugger.js +0 -755
  222. package/src/dx/visual-pipeline-builder.js +0 -578
  223. package/src/ingest.js +0 -59
  224. package/src/pipeline/pipeline.js +0 -18
  225. package/src/query.js +0 -117
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const FORMAT_VERSION = 1;
7
+
8
+ /**
9
+ * Vector store persisted to a single JSON file, so separate processes (e.g.
10
+ * `rag-pipeline ingest` then `rag-pipeline query`) share one index. Intended
11
+ * for local use and small corpora: every query is a linear scan.
12
+ *
13
+ * The index records which embedder produced the vectors (name, dimensions and
14
+ * optional serialized state) and refuses queries whose vector dimension does
15
+ * not match, instead of silently comparing incompatible vectors.
16
+ */
17
+ class FileVectorStore {
18
+ /**
19
+ * @param {object} options
20
+ * @param {string} options.path - Index file location
21
+ */
22
+ constructor(options = {}) {
23
+ if (!options.path) {
24
+ throw new Error('FileVectorStore requires a `path` option');
25
+ }
26
+ this.path = path.resolve(options.path);
27
+ this._index = null;
28
+ }
29
+
30
+ _emptyIndex() {
31
+ return { version: FORMAT_VERSION, embedder: null, documents: {} };
32
+ }
33
+
34
+ /**
35
+ * Read the index from disk (cached after the first call).
36
+ * @returns {{version: number, embedder: object|null, documents: object}}
37
+ */
38
+ load() {
39
+ if (this._index) return this._index;
40
+ if (!fs.existsSync(this.path)) {
41
+ this._index = this._emptyIndex();
42
+ return this._index;
43
+ }
44
+ const parsed = JSON.parse(fs.readFileSync(this.path, 'utf8'));
45
+ if (parsed.version !== FORMAT_VERSION || !parsed.documents) {
46
+ throw new Error(
47
+ `Unsupported index format in ${this.path} (version ${parsed.version})`,
48
+ );
49
+ }
50
+ this._index = parsed;
51
+ return this._index;
52
+ }
53
+
54
+ /** Write the index atomically: temp file in the same directory, then rename. */
55
+ save() {
56
+ const index = this.load();
57
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
58
+ const tmp = `${this.path}.${process.pid}.tmp`;
59
+ fs.writeFileSync(tmp, JSON.stringify(index));
60
+ fs.renameSync(tmp, this.path);
61
+ }
62
+
63
+ /** @returns {object|null} Embedder metadata recorded with the vectors */
64
+ getEmbedderInfo() {
65
+ return this.load().embedder;
66
+ }
67
+
68
+ /** @param {{name: string, dimensions: number, state?: object}} info */
69
+ setEmbedderInfo(info) {
70
+ this.load().embedder = info;
71
+ }
72
+
73
+ /**
74
+ * Add or replace documents. Ids are caller-chosen; re-storing an id
75
+ * replaces the previous entry.
76
+ *
77
+ * @param {Array<{id: string, vector: number[], content: string, metadata?: object}>} docs
78
+ */
79
+ async store(docs) {
80
+ const { documents } = this.load();
81
+ for (const doc of docs) {
82
+ if (!doc.id || !Array.isArray(doc.vector)) {
83
+ throw new Error(
84
+ 'FileVectorStore.store(): each doc needs id and vector',
85
+ );
86
+ }
87
+ documents[doc.id] = {
88
+ vector: encodeVector(doc.vector),
89
+ content: doc.content || '',
90
+ metadata: doc.metadata || {},
91
+ };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Remove every document whose metadata.source equals `source`.
97
+ * @param {string} source
98
+ * @returns {number} Number of documents removed
99
+ */
100
+ removeBySource(source) {
101
+ const { documents } = this.load();
102
+ let removed = 0;
103
+ for (const [id, doc] of Object.entries(documents)) {
104
+ if (doc.metadata?.source === source) {
105
+ delete documents[id];
106
+ removed++;
107
+ }
108
+ }
109
+ return removed;
110
+ }
111
+
112
+ /** Remove every document and the recorded embedder info. */
113
+ clear() {
114
+ this._index = this._emptyIndex();
115
+ }
116
+
117
+ /** @returns {Array<{id: string, content: string, metadata: object}>} */
118
+ list() {
119
+ return Object.entries(this.load().documents).map(([id, doc]) => ({
120
+ id,
121
+ content: doc.content,
122
+ metadata: doc.metadata,
123
+ }));
124
+ }
125
+
126
+ /** @returns {number} */
127
+ size() {
128
+ return Object.keys(this.load().documents).length;
129
+ }
130
+
131
+ /**
132
+ * Cosine-similarity search.
133
+ *
134
+ * @param {{queryVector: number[], topK?: number}} request
135
+ * @returns {Promise<Array<{id: string, score: number, content: string, metadata: object}>>}
136
+ */
137
+ async retrieve(request = {}) {
138
+ const { queryVector, topK = 5 } = request;
139
+ if (!Array.isArray(queryVector)) {
140
+ throw new Error(
141
+ 'FileVectorStore.retrieve(): queryVector is required. Configure an embedder stage.',
142
+ );
143
+ }
144
+ const info = this.getEmbedderInfo();
145
+ if (info && info.dimensions && info.dimensions !== queryVector.length) {
146
+ throw new Error(
147
+ `Query vector has ${queryVector.length} dimensions but the index was built ` +
148
+ `with ${info.dimensions} (embedder "${info.name}"). Re-run ingest with the ` +
149
+ 'current embedder.',
150
+ );
151
+ }
152
+
153
+ const queryMagnitude = Math.sqrt(
154
+ queryVector.reduce((sum, x) => sum + x * x, 0),
155
+ );
156
+ const results = Object.entries(this.load().documents).map(([id, doc]) => ({
157
+ id,
158
+ score: cosine(queryVector, queryMagnitude, doc.vector),
159
+ content: doc.content,
160
+ metadata: doc.metadata,
161
+ }));
162
+ results.sort((a, b) => b.score - a.score);
163
+ return results.slice(0, topK);
164
+ }
165
+ }
166
+
167
+ // Stored vectors keep ~7 significant digits; more only inflates the file.
168
+ const round = (x) => Number(x.toPrecision(7));
169
+
170
+ /**
171
+ * Mostly-zero vectors (e.g. LocalEmbedder's TF-IDF over a large vocabulary)
172
+ * are stored sparsely as { n, i, v }; dense ones as plain arrays.
173
+ */
174
+ function encodeVector(vector) {
175
+ const indices = [];
176
+ const values = [];
177
+ for (let i = 0; i < vector.length; i++) {
178
+ if (vector[i] !== 0) {
179
+ indices.push(i);
180
+ values.push(round(vector[i]));
181
+ }
182
+ }
183
+ if (indices.length < vector.length / 4) {
184
+ return { n: vector.length, i: indices, v: values };
185
+ }
186
+ return vector.map(round);
187
+ }
188
+
189
+ /** Cosine similarity between a dense query and a stored (dense or sparse) vector. */
190
+ function cosine(query, magQuery, stored) {
191
+ const sparse = !Array.isArray(stored);
192
+ const length = sparse ? stored.n : stored.length;
193
+ if (query.length !== length) return 0;
194
+
195
+ let dot = 0;
196
+ let magStored = 0;
197
+ if (sparse) {
198
+ for (let k = 0; k < stored.i.length; k++) {
199
+ dot += query[stored.i[k]] * stored.v[k];
200
+ magStored += stored.v[k] * stored.v[k];
201
+ }
202
+ } else {
203
+ for (let k = 0; k < length; k++) {
204
+ dot += query[k] * stored[k];
205
+ magStored += stored[k] * stored[k];
206
+ }
207
+ }
208
+ const magnitude = Math.sqrt(magQuery) * Math.sqrt(magStored);
209
+ return magnitude === 0 ? 0 : dot / magnitude;
210
+ }
211
+
212
+ module.exports = { FileVectorStore };
@@ -33,26 +33,23 @@ class HybridRetriever extends EventEmitter {
33
33
  return [];
34
34
  }
35
35
 
36
- const topK = (query && query.topK) || this.config.topK;
37
-
38
- // Run all retrievers in parallel
39
- const promises = this.retrievers.map(({ retriever, name }) => {
40
- const fn =
41
- typeof retriever.retrieve === 'function'
42
- ? retriever.retrieve.bind(retriever)
43
- : typeof retriever.search === 'function'
44
- ? retriever.search.bind(retriever)
45
- : null;
46
-
47
- if (!fn) {
48
- return Promise.reject(
49
- new Error(`Retriever "${name}" has no retrieve() or search() method`),
50
- );
36
+ const request = typeof query === 'string' ? { query } : { ...query };
37
+ const topK = request.topK || this.config.topK;
38
+ request.topK = topK;
39
+
40
+ // Run all retrievers in parallel. retrieve() follows the retriever
41
+ // contract and gets the full request (so vector retrievers receive
42
+ // queryVector); search() is the BM25-style (text, { k }) signature.
43
+ const promises = this.retrievers.map(async ({ retriever, name }) => {
44
+ if (typeof retriever.retrieve === 'function') {
45
+ return retriever.retrieve(request);
51
46
  }
52
-
53
- // Pass query string or the full query object depending on method
54
- const arg = typeof query === 'string' ? query : query.query || query;
55
- return fn(arg);
47
+ if (typeof retriever.search === 'function') {
48
+ return retriever.search(request.query, { k: topK });
49
+ }
50
+ throw new Error(
51
+ `Retriever "${name}" has no retrieve() or search() method`,
52
+ );
56
53
  });
57
54
 
58
55
  const settled = await Promise.allSettled(promises);
@@ -77,6 +74,11 @@ class HybridRetriever extends EventEmitter {
77
74
  }
78
75
  }
79
76
 
77
+ if (successCount === 0) {
78
+ const reasons = settled.map((o) => o.reason?.message).join('; ');
79
+ throw new Error(`HybridRetriever: all retrievers failed (${reasons})`);
80
+ }
81
+
80
82
  const fused = reciprocalRankFusion(resultSets, {
81
83
  k: this.config.fusionK,
82
84
  weights,
@@ -87,6 +89,7 @@ class HybridRetriever extends EventEmitter {
87
89
  this.emit('retrieved', {
88
90
  totalRetrievers: this.retrievers.length,
89
91
  successfulRetrievers: successCount,
92
+ failedRetrievers: this.retrievers.length - successCount,
90
93
  resultCount: topResults.length,
91
94
  });
92
95
 
@@ -15,6 +15,16 @@ const fs = require('fs');
15
15
  const path = require('path');
16
16
  const { logger } = require('../utils/logger');
17
17
 
18
+ /**
19
+ * Parse an integer env value; unset or invalid values use the fallback but
20
+ * an explicit 0 is kept (unlike `parseInt(v) || fallback`).
21
+ * @private
22
+ */
23
+ function intOrDefault(value, fallback) {
24
+ const parsed = parseInt(value, 10);
25
+ return Number.isNaN(parsed) ? fallback : parsed;
26
+ }
27
+
18
28
  /**
19
29
  * Load JWT configuration from environment or defaults
20
30
  *
@@ -145,9 +155,10 @@ function loadJWTConfig() {
145
155
  audience: process.env.JWT_AUDIENCE || 'rag-pipeline-api',
146
156
 
147
157
  // Timing configuration
148
- clockTolerance: parseInt(process.env.JWT_CLOCK_TOLERANCE, 10) || 30,
158
+ // An explicit 0 is honoured (only unset/invalid values fall back to 30)
159
+ clockTolerance: intOrDefault(process.env.JWT_CLOCK_TOLERANCE, 30),
149
160
  maxTokenAge: parseInt(process.env.JWT_EXPIRY, 10) || 3600, // 1 hour
150
- notBeforeTolerance: parseInt(process.env.JWT_NBF_TOLERANCE, 10) || 30,
161
+ notBeforeTolerance: intOrDefault(process.env.JWT_NBF_TOLERANCE, 30),
151
162
 
152
163
  // Security options
153
164
  requireJti: process.env.JWT_REQUIRE_JTI !== 'false',
@@ -11,6 +11,21 @@
11
11
  * - Replay attacks (with JTI tracking)
12
12
  * - Token expiration and timing issues
13
13
  *
14
+ * Replay protection (default): when `enableJtiTracking` is on (the default)
15
+ * and `singleUseTokens` is on (the default), every externally issued token
16
+ * (one this validator did not sign) is single-use: a second verify() of the
17
+ * same JTI throws "Token replay detected". Tokens signed by this validator
18
+ * instance may be verified repeatedly. To accept ordinary bearer-token reuse
19
+ * (the same access token presented on many requests until it expires), pass
20
+ * `singleUseTokens: false`; JTIs are then not recorded on verify().
21
+ *
22
+ * The JTI store is bounded: at most `maxTrackedJtis` entries (default
23
+ * 100000) are kept, each entry expires at the token's `exp` (plus the clock
24
+ * tolerance), expired entries are dropped first and, if the store is still
25
+ * full, the least-recently-used entry is evicted (a `jti_evicted` event is
26
+ * emitted). An evicted external JTI could be replayed until it expires, so
27
+ * size `maxTrackedJtis` for your peak number of live tokens.
28
+ *
14
29
  * @module security/jwt-validator
15
30
  * @since 2.3.0
16
31
  */
@@ -91,9 +106,10 @@ class JWTValidator extends EventEmitter {
91
106
  audience: options.audience,
92
107
 
93
108
  // Timing configuration
94
- clockTolerance: options.clockTolerance || 30, // seconds
109
+ // `??` so an explicit 0 is honoured (`||` silently turned 0 into 30).
110
+ clockTolerance: options.clockTolerance ?? 30, // seconds
95
111
  maxTokenAge: options.maxTokenAge || 3600, // 1 hour in seconds
96
- notBeforeTolerance: options.notBeforeTolerance || 30, // seconds
112
+ notBeforeTolerance: options.notBeforeTolerance ?? 30, // seconds
97
113
 
98
114
  // Security options
99
115
  requireJti: options.requireJti !== false, // Require JWT ID by default
@@ -104,6 +120,11 @@ class JWTValidator extends EventEmitter {
104
120
  // JTI tracking for replay protection
105
121
  enableJtiTracking: options.enableJtiTracking !== false,
106
122
  jtiExpirationMs: options.jtiExpirationMs || 7 * 24 * 60 * 60 * 1000, // 7 days
123
+ // Treat external tokens as single-use (default). Set to false to allow
124
+ // normal bearer-token reuse until the token expires.
125
+ singleUseTokens: options.singleUseTokens !== false,
126
+ // Upper bound on tracked JTIs (LRU eviction beyond this).
127
+ maxTrackedJtis: options.maxTrackedJtis ?? 100000,
107
128
 
108
129
  // Additional validation
109
130
  validateClaims: options.validateClaims || {},
@@ -115,6 +136,13 @@ class JWTValidator extends EventEmitter {
115
136
  // Validate configuration
116
137
  this._validateConfig();
117
138
 
139
+ if (
140
+ !Number.isInteger(this.config.maxTrackedJtis) ||
141
+ this.config.maxTrackedJtis < 1
142
+ ) {
143
+ throw new Error('maxTrackedJtis must be a positive integer');
144
+ }
145
+
118
146
  // Initialize JTI tracking
119
147
  if (this.config.enableJtiTracking) {
120
148
  this.usedJtis = new Map(); // jti -> expiration timestamp (for external tokens)
@@ -300,10 +328,9 @@ class JWTValidator extends EventEmitter {
300
328
  // Self-signed tokens are tracked to prevent JTI collisions with external tokens
301
329
  // but they can be verified multiple times (marked as reusable)
302
330
  if (this.config.enableJtiTracking && jti) {
303
- this.selfSignedJtis.add(jti);
304
331
  // Also track in usedJtis with reusable flag to prevent external tokens from using same JTI
305
332
  const expirationTime = (now + expiresIn) * 1000;
306
- this.usedJtis.set(jti, { expirationTime, reusable: true });
333
+ this._trackJti(jti, { expirationTime, reusable: true });
307
334
  }
308
335
 
309
336
  // Update statistics
@@ -417,22 +444,27 @@ class JWTValidator extends EventEmitter {
417
444
  // - External tokens (non-reusable): if race condition occurs, both requests succeed on first use
418
445
  // (acceptable) but subsequent replays are blocked (secure)
419
446
  if (this.config.enableJtiTracking && payload.jti) {
420
- // Check if this is a self-signed (reusable) token
421
- const isSelfSigned = this.selfSignedJtis.has(payload.jti);
422
-
423
- if (!this.usedJtis.has(payload.jti)) {
424
- // First time seeing this JTI - atomically track it
425
- const expirationTime = payload.exp * 1000;
426
- this.usedJtis.set(payload.jti, {
427
- expirationTime,
428
- reusable: isSelfSigned,
429
- });
447
+ const jtiEntry = this._getJtiEntry(payload.jti);
448
+ // Self-signed (reusable) tokens are recorded at sign() time
449
+ const isSelfSigned = Boolean(
450
+ jtiEntry && jtiEntry.reusable && this.selfSignedJtis.has(payload.jti),
451
+ );
452
+
453
+ if (!jtiEntry) {
454
+ // First time seeing this JTI. Record it only when single-use
455
+ // checking is on; with singleUseTokens: false bearer tokens may be
456
+ // presented repeatedly until they expire.
457
+ if (this.config.singleUseTokens) {
458
+ this._trackJti(payload.jti, {
459
+ expirationTime: payload.exp * 1000,
460
+ reusable: false,
461
+ });
462
+ }
430
463
  } else {
431
464
  // JTI has been seen before
432
- const jtiEntry = this.usedJtis.get(payload.jti);
433
465
 
434
466
  // Self-signed tokens are reusable - allow multiple verifications
435
- if (jtiEntry.reusable && isSelfSigned) {
467
+ if (isSelfSigned) {
436
468
  // This is valid for refresh flows and load balancer retries
437
469
  } else {
438
470
  // External token being replayed - this is a security violation
@@ -583,6 +615,91 @@ class JWTValidator extends EventEmitter {
583
615
  }
584
616
  }
585
617
 
618
+ /**
619
+ * Expiry time (ms) of a JTI store entry, including clock tolerance.
620
+ * @private
621
+ */
622
+ _jtiExpiry(jtiEntry) {
623
+ // Handle both old format (number) and new format (object)
624
+ const expirationTime =
625
+ typeof jtiEntry === 'number' ? jtiEntry : jtiEntry.expirationTime;
626
+ return expirationTime + this.config.clockTolerance * 1000;
627
+ }
628
+
629
+ /**
630
+ * Remove a JTI from both tracking structures.
631
+ * @private
632
+ */
633
+ _forgetJti(jti) {
634
+ this.usedJtis.delete(jti);
635
+ this.selfSignedJtis.delete(jti);
636
+ }
637
+
638
+ /**
639
+ * Look up a JTI, dropping it if it has expired and refreshing its LRU
640
+ * position otherwise.
641
+ * @private
642
+ */
643
+ _getJtiEntry(jti) {
644
+ const entry = this.usedJtis.get(jti);
645
+ if (entry === undefined) return undefined;
646
+ if (this._jtiExpiry(entry) < Date.now()) {
647
+ this._forgetJti(jti);
648
+ return undefined;
649
+ }
650
+ // Map preserves insertion order: delete + set moves it to most-recent.
651
+ this.usedJtis.delete(jti);
652
+ this.usedJtis.set(jti, entry);
653
+ return entry;
654
+ }
655
+
656
+ /**
657
+ * Record a JTI in the bounded store. When the store is full, expired
658
+ * entries are purged first; if it is still full the least-recently-used
659
+ * entry is evicted.
660
+ * @private
661
+ */
662
+ _trackJti(jti, entry) {
663
+ this._forgetJti(jti);
664
+
665
+ if (this.usedJtis.size >= this.config.maxTrackedJtis) {
666
+ this._purgeExpiredJtis();
667
+ }
668
+
669
+ while (this.usedJtis.size >= this.config.maxTrackedJtis) {
670
+ const oldest = this.usedJtis.keys().next().value;
671
+ this._forgetJti(oldest);
672
+ this.emit('jti_evicted', {
673
+ jti: oldest,
674
+ reason: 'capacity',
675
+ maxTrackedJtis: this.config.maxTrackedJtis,
676
+ timestamp: new Date().toISOString(),
677
+ });
678
+ }
679
+
680
+ this.usedJtis.set(jti, entry);
681
+ if (entry.reusable) {
682
+ this.selfSignedJtis.add(jti);
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Drop every expired JTI from the store.
688
+ * @private
689
+ * @returns {number} Number of entries removed
690
+ */
691
+ _purgeExpiredJtis() {
692
+ const now = Date.now();
693
+ let cleanedCount = 0;
694
+ for (const [jti, jtiEntry] of this.usedJtis.entries()) {
695
+ if (this._jtiExpiry(jtiEntry) < now) {
696
+ this._forgetJti(jti);
697
+ cleanedCount++;
698
+ }
699
+ }
700
+ return cleanedCount;
701
+ }
702
+
586
703
  /**
587
704
  * Start periodic cleanup of expired JTIs
588
705
  * @private
@@ -590,20 +707,7 @@ class JWTValidator extends EventEmitter {
590
707
  _startJtiCleanup() {
591
708
  this.jtiCleanupInterval = setInterval(
592
709
  () => {
593
- const now = Date.now();
594
- let cleanedCount = 0;
595
-
596
- for (const [jti, jtiEntry] of this.usedJtis.entries()) {
597
- // Handle both old format (number) and new format (object)
598
- const expirationTime =
599
- typeof jtiEntry === 'number' ? jtiEntry : jtiEntry.expirationTime;
600
- if (expirationTime < now) {
601
- this.usedJtis.delete(jti);
602
- // Also remove from selfSignedJtis if it's there
603
- this.selfSignedJtis.delete(jti);
604
- cleanedCount++;
605
- }
606
- }
710
+ const cleanedCount = this._purgeExpiredJtis();
607
711
 
608
712
  if (cleanedCount > 0 && this.config.enableAuditLog) {
609
713
  this.emit('jti_cleanup', {
@@ -7,7 +7,23 @@
7
7
  * @version 2.1.1
8
8
  */
9
9
 
10
- const ivm = require('isolated-vm');
10
+ // isolated-vm is a native addon and an optional peer dependency: load it only
11
+ // when a sandbox is actually used so importing the package never requires it.
12
+ let ivmModule = null;
13
+ function loadIsolatedVm() {
14
+ if (!ivmModule) {
15
+ try {
16
+ ivmModule = require('isolated-vm');
17
+ } catch (error) {
18
+ throw new Error(
19
+ 'PluginSandbox requires the optional peer dependency isolated-vm. ' +
20
+ 'Install it with `npm install isolated-vm`. ' +
21
+ `(${error.message})`,
22
+ );
23
+ }
24
+ }
25
+ return ivmModule;
26
+ }
11
27
  const fs = require('fs');
12
28
  const path = require('path');
13
29
  const crypto = require('crypto');
@@ -295,6 +311,7 @@ class PluginSandbox extends EventEmitter {
295
311
  let context;
296
312
 
297
313
  try {
314
+ const ivm = loadIsolatedVm();
298
315
  // Create new isolate with memory limit
299
316
  isolate = new ivm.Isolate({
300
317
  memoryLimit: Math.floor(this.options.memoryLimit / (1024 * 1024)), // Convert to MB