@devilsdev/rag-pipeline-utils 2.4.5 → 2.6.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 (258) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +431 -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 +6442 -1355
  9. package/dist/index.d.ts +129 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/testing.cjs +2 -0
  12. package/dist/testing.d.ts +1 -0
  13. package/dist/testing.mjs +5 -0
  14. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  15. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  16. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  17. package/dist/types/agentic/query-planner.d.ts +145 -0
  18. package/dist/types/agentic/self-critique.d.ts +45 -0
  19. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  20. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  21. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  22. package/dist/types/ai/multimodal.d.ts +39 -0
  23. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  24. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  25. package/dist/types/cache/component-keys.d.ts +7 -0
  26. package/dist/types/cache/memory-cache.d.ts +54 -0
  27. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  28. package/dist/types/cache/redis-cache.d.ts +46 -0
  29. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  30. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  31. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  32. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  33. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  34. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  35. package/dist/types/citation/citation-tracker.d.ts +64 -0
  36. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  37. package/dist/types/citation/source-mapper.d.ts +37 -0
  38. package/dist/types/connectors/anthropic-connector.d.ts +97 -0
  39. package/dist/types/connectors/base-connector.d.ts +87 -0
  40. package/dist/types/connectors/cohere-connector.d.ts +94 -0
  41. package/dist/types/connectors/connector-utils.d.ts +110 -0
  42. package/dist/types/connectors/local-embedder.d.ts +95 -0
  43. package/dist/types/connectors/memory-retriever.d.ts +99 -0
  44. package/dist/types/connectors/ollama-connector.d.ts +115 -0
  45. package/dist/types/connectors/openai-connector.d.ts +129 -0
  46. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  47. package/dist/types/core/create-pipeline.d.ts +8 -0
  48. package/dist/types/core/plugin-registry.d.ts +136 -0
  49. package/dist/types/core/stage-contracts.d.ts +36 -0
  50. package/dist/types/core/validate-plugin.d.ts +45 -0
  51. package/dist/types/cost/cost-calculator.d.ts +152 -0
  52. package/dist/types/cost/cost-tracker.d.ts +126 -0
  53. package/dist/types/cost/token-budget.d.ts +110 -0
  54. package/dist/types/dag/core/error-context.d.ts +239 -0
  55. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  56. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  57. package/dist/types/dag/dag-engine.d.ts +156 -0
  58. package/dist/types/dag/dag-node.d.ts +37 -0
  59. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  60. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  61. package/dist/types/debug/execution-tracer.d.ts +57 -0
  62. package/dist/types/debug/trace-inspector.d.ts +31 -0
  63. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  64. package/dist/types/enterprise/data-governance.d.ts +203 -0
  65. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  66. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  67. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  68. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  69. package/dist/types/evaluate/groundedness.d.ts +60 -0
  70. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  71. package/dist/types/evaluate/relevance.d.ts +21 -0
  72. package/dist/types/evaluate/scoring.d.ts +123 -0
  73. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  74. package/dist/types/experimental.d.ts +15 -0
  75. package/dist/types/graph/entity-extractor.d.ts +84 -0
  76. package/dist/types/graph/graph-index.d.ts +49 -0
  77. package/dist/types/graph/graph-retriever.d.ts +81 -0
  78. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  79. package/dist/types/grounding/check-answer.d.ts +101 -0
  80. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  81. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  82. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  83. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  84. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  85. package/dist/types/mcp/mcp-server.d.ts +85 -0
  86. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  87. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  88. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  89. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  90. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  91. package/dist/types/retrieval/file-vector-store.d.ts +99 -0
  92. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  93. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  94. package/dist/types/security/jwt-config.d.ts +80 -0
  95. package/dist/types/security/jwt-validator.d.ts +195 -0
  96. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  97. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  98. package/dist/types/security/validator.d.ts +5 -0
  99. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  100. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  101. package/dist/types/streaming/stream-router.d.ts +88 -0
  102. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  103. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  104. package/dist/types/templates/scaffold.d.ts +55 -0
  105. package/dist/types/templates/template-registry.d.ts +63 -0
  106. package/dist/types/testing.d.ts +168 -0
  107. package/dist/types/utils/embed-query.d.ts +20 -0
  108. package/dist/types/utils/logger.d.ts +50 -0
  109. package/dist/types/utils/rate-limiter.d.ts +66 -0
  110. package/dist/types/utils/retry.d.ts +27 -0
  111. package/dist/types/utils/run-abort.d.ts +34 -0
  112. package/dist/types/utils/secure-logging.d.ts +51 -0
  113. package/dist/types/utils/structured-logger.d.ts +126 -0
  114. package/package.json +72 -33
  115. package/src/adapters/ai-sdk.js +215 -0
  116. package/src/agentic/agentic-pipeline.js +75 -7
  117. package/src/agentic/iterative-retriever.js +47 -10
  118. package/src/agentic/query-planner.js +329 -78
  119. package/src/cache/component-keys.js +38 -0
  120. package/src/cache/memory-cache.js +87 -0
  121. package/src/cache/pipeline-cache.js +260 -0
  122. package/src/cache/redis-cache.js +83 -0
  123. package/src/citation/citation-tracker.js +6 -4
  124. package/src/citation/hallucination-detector.js +2 -2
  125. package/src/citation/source-mapper.js +15 -28
  126. package/src/cli/commands/ai.js +106 -0
  127. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  128. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  129. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  130. package/src/cli/commands/plugin-hub.js +114 -49
  131. package/src/cli/diagnostics/config-checker.js +76 -63
  132. package/src/cli/diagnostics/environment-checker.js +62 -31
  133. package/src/cli/diagnostics/plugin-checker.js +36 -89
  134. package/src/cli/doctor-command.js +39 -5
  135. package/src/cli/enhanced-cli-commands.js +104 -233
  136. package/src/cli/handlers/ai-handler.js +364 -0
  137. package/src/cli/handlers/diagnostics-handler.js +78 -31
  138. package/src/cli/handlers/ingest-handler.js +28 -22
  139. package/src/cli/handlers/init-handler.js +148 -25
  140. package/src/cli/handlers/query-handler.js +203 -25
  141. package/src/cli/handlers/run-handler.js +16 -167
  142. package/src/cli/handlers/serve-handler.js +60 -0
  143. package/src/cli/services/bench-stats.js +54 -0
  144. package/src/cli/services/benchmark.js +161 -0
  145. package/src/cli/services/feedback.js +155 -0
  146. package/src/cli/services/instrument.js +83 -0
  147. package/src/cli/services/rag-service.js +361 -0
  148. package/src/cli/shared/cli-utils.js +24 -7
  149. package/src/cli/shared/config-context.js +21 -0
  150. package/src/cli/wizard/config-builder.js +81 -0
  151. package/src/cli/wizard/init-wizard.js +602 -0
  152. package/src/config/component-factory.js +125 -0
  153. package/src/config/enhanced-ragrc-schema.js +0 -2
  154. package/src/config/load-config.js +29 -10
  155. package/src/config/load-plugin-config.js +46 -2
  156. package/src/config/validate-schema.js +4 -1
  157. package/src/connectors/anthropic-connector.js +115 -57
  158. package/src/connectors/base-connector.js +74 -19
  159. package/src/connectors/cohere-connector.js +108 -34
  160. package/src/connectors/connector-utils.js +302 -0
  161. package/src/connectors/local-embedder.js +63 -5
  162. package/src/connectors/memory-retriever.js +125 -14
  163. package/src/connectors/ollama-connector.js +112 -51
  164. package/src/connectors/openai-connector.js +153 -44
  165. package/src/connectors/rag-prompt.js +88 -0
  166. package/src/core/create-pipeline.js +307 -61
  167. package/src/core/performance/parallel-processor.js +62 -1
  168. package/src/core/plugin-registry.js +125 -26
  169. package/src/core/stage-contracts.js +36 -0
  170. package/src/core/validate-plugin.js +85 -0
  171. package/src/cost/cost-calculator.js +205 -32
  172. package/src/debug/execution-tracer.js +14 -4
  173. package/src/dev/dev-server.js +173 -11
  174. package/src/dev/hot-reload.js +44 -4
  175. package/src/ecosystem/plugin-checks.js +181 -0
  176. package/src/ecosystem/plugin-hub.js +50 -125
  177. package/src/ecosystem/plugin-installer.js +298 -0
  178. package/src/ecosystem/plugin-probe.js +127 -0
  179. package/src/ecosystem/plugin-scanner.js +185 -0
  180. package/src/ecosystem/tar-extract.js +114 -0
  181. package/src/enterprise/audit-logging.js +158 -29
  182. package/src/enterprise/sso-integration.js +716 -228
  183. package/src/evaluate/context-metrics.js +28 -16
  184. package/src/evaluate/evaluator.js +3 -3
  185. package/src/evaluate/faithfulness.js +18 -14
  186. package/src/evaluate/groundedness.js +43 -12
  187. package/src/evaluate/pipeline-evaluator.js +35 -8
  188. package/src/evaluate/scoring.js +251 -3
  189. package/src/evaluate/semantic-similarity.js +79 -0
  190. package/src/experimental.js +71 -0
  191. package/src/grounding/check-answer.js +173 -0
  192. package/src/guardrails/guardrails-pipeline.js +69 -7
  193. package/src/guardrails/injection-detector.js +283 -0
  194. package/src/guardrails/post-generation-guard.js +19 -0
  195. package/src/guardrails/pre-retrieval-guard.js +36 -36
  196. package/src/guardrails/retrieval-guard.js +58 -8
  197. package/src/index.js +58 -1
  198. package/src/loader/csv-loader.js +3 -2
  199. package/src/loader/document-reader.js +91 -0
  200. package/src/loader/html-loader.js +3 -1
  201. package/src/loader/markdown-loader.js +3 -2
  202. package/src/reranker/cascade-reranker.js +3 -0
  203. package/src/reranker/embedding-reranker.js +136 -21
  204. package/src/reranker/llm-reranker.js +79 -12
  205. package/src/reranker/scoring-reranker.js +3 -0
  206. package/src/retrieval/bm25-search.js +12 -6
  207. package/src/retrieval/file-vector-store.js +241 -0
  208. package/src/retrieval/hybrid-retriever.js +22 -19
  209. package/src/security/jwt-config.js +13 -2
  210. package/src/security/jwt-validator.js +139 -34
  211. package/src/security/plugin-sandbox.js +18 -1
  212. package/src/security/plugin-signature-verifier.js +245 -70
  213. package/src/testing.js +254 -0
  214. package/src/utils/deprecate.js +69 -0
  215. package/src/utils/embed-query.js +33 -0
  216. package/src/utils/input-sanitizer.js +19 -4
  217. package/src/utils/logger.js +34 -4
  218. package/src/utils/run-abort.js +102 -0
  219. package/src/ai/federated-learning.js +0 -211
  220. package/src/ai/federation.js +0 -71
  221. package/src/ai/index.js +0 -22
  222. package/src/ai/model-trainer.js +0 -217
  223. package/src/ai/model-training.js +0 -202
  224. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  225. package/src/ai/orchestrator.js +0 -65
  226. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  227. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  228. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  229. package/src/cli/commands/ai-ml/index.js +0 -17
  230. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  231. package/src/cli/commands/ai-ml/shared.js +0 -51
  232. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  233. package/src/cli/commands/ai-ml.js +0 -386
  234. package/src/cli/commands/docs.js +0 -228
  235. package/src/cli/commands/dx.js +0 -538
  236. package/src/cli/commands/wizard.js +0 -627
  237. package/src/cli/input-sanitization-middleware.js +0 -356
  238. package/src/cli/interactive-wizard.js +0 -373
  239. package/src/cli/templates/development.template.json +0 -64
  240. package/src/cli/templates/minimal.template.json +0 -33
  241. package/src/cli/templates/production.template.json +0 -104
  242. package/src/cli/templates/testing.template.json +0 -45
  243. package/src/cli/wizard/configuration.js +0 -148
  244. package/src/cli/wizard/observability.js +0 -79
  245. package/src/cli/wizard/performance.js +0 -76
  246. package/src/cli/wizard/plugin-selection.js +0 -297
  247. package/src/cli/wizard/preview-save.js +0 -172
  248. package/src/cli/wizard/project-setup.js +0 -66
  249. package/src/core/pipeline-factory.js +0 -59
  250. package/src/dag/enhanced-dag-engine.js +0 -894
  251. package/src/dx/index.js +0 -40
  252. package/src/dx/integration-templates.js +0 -662
  253. package/src/dx/performance-profiler.js +0 -968
  254. package/src/dx/realtime-debugger.js +0 -755
  255. package/src/dx/visual-pipeline-builder.js +0 -578
  256. package/src/ingest.js +0 -59
  257. package/src/pipeline/pipeline.js +0 -18
  258. package/src/query.js +0 -117
@@ -0,0 +1,241 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { randomBytes } = require('crypto');
6
+ const { digest } = require('../cache/component-keys');
7
+
8
+ const FORMAT_VERSION = 1;
9
+
10
+ /**
11
+ * Vector store persisted to a single JSON file, so separate processes (e.g.
12
+ * `rag-pipeline ingest` then `rag-pipeline query`) share one index. Intended
13
+ * for local use and small corpora: every query is a linear scan.
14
+ *
15
+ * The index records which embedder produced the vectors (name, dimensions and
16
+ * optional serialized state) and refuses queries whose vector dimension does
17
+ * not match, instead of silently comparing incompatible vectors.
18
+ */
19
+ class FileVectorStore {
20
+ /**
21
+ * @param {object} options
22
+ * @param {string} options.path - Index file location
23
+ */
24
+ constructor(options = {}) {
25
+ if (!options.path) {
26
+ throw new Error('FileVectorStore requires a `path` option');
27
+ }
28
+ this.path = path.resolve(options.path);
29
+ this._index = null;
30
+ this._loadedDigest = null;
31
+ this._cacheId = randomBytes(8).toString('hex');
32
+ this._revision = 0;
33
+ }
34
+
35
+ /**
36
+ * Cache identity for createRagPipeline({ cache }): a digest of the index
37
+ * as loaded, so processes reading the same unchanged index share cached
38
+ * results; after this instance changes the index the key also carries the
39
+ * instance and a revision.
40
+ * @param {'retriever'} role
41
+ * @returns {string|null}
42
+ */
43
+ getCacheKey(role) {
44
+ if (role !== 'retriever') return null;
45
+ this.load();
46
+ return this._revision === 0
47
+ ? `file|${this._loadedDigest}`
48
+ : `file|${this._loadedDigest}|${this._cacheId}|${this._revision}`;
49
+ }
50
+
51
+ _emptyIndex() {
52
+ return { version: FORMAT_VERSION, embedder: null, documents: {} };
53
+ }
54
+
55
+ /**
56
+ * Read the index from disk (cached after the first call).
57
+ * @returns {{version: number, embedder: object|null, documents: object}}
58
+ */
59
+ load() {
60
+ if (this._index) return this._index;
61
+ if (!fs.existsSync(this.path)) {
62
+ this._index = this._emptyIndex();
63
+ this._loadedDigest = 'empty';
64
+ return this._index;
65
+ }
66
+ const raw = fs.readFileSync(this.path, 'utf8');
67
+ const parsed = JSON.parse(raw);
68
+ if (parsed.version !== FORMAT_VERSION || !parsed.documents) {
69
+ throw new Error(
70
+ `Unsupported index format in ${this.path} (version ${parsed.version})`,
71
+ );
72
+ }
73
+ this._index = parsed;
74
+ this._loadedDigest = digest(raw);
75
+ return this._index;
76
+ }
77
+
78
+ /** Write the index atomically: temp file in the same directory, then rename. */
79
+ save() {
80
+ const index = this.load();
81
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
82
+ const tmp = `${this.path}.${process.pid}.tmp`;
83
+ fs.writeFileSync(tmp, JSON.stringify(index));
84
+ fs.renameSync(tmp, this.path);
85
+ }
86
+
87
+ /** @returns {object|null} Embedder metadata recorded with the vectors */
88
+ getEmbedderInfo() {
89
+ return this.load().embedder;
90
+ }
91
+
92
+ /** @param {{name: string, dimensions: number, state?: object}} info */
93
+ setEmbedderInfo(info) {
94
+ this.load().embedder = info;
95
+ this._revision += 1;
96
+ }
97
+
98
+ /**
99
+ * Add or replace documents. Ids are caller-chosen; re-storing an id
100
+ * replaces the previous entry.
101
+ *
102
+ * @param {Array<{id: string, vector: number[], content: string, metadata?: object}>} docs
103
+ */
104
+ async store(docs) {
105
+ const { documents } = this.load();
106
+ this._revision += 1;
107
+ for (const doc of docs) {
108
+ if (!doc.id || !Array.isArray(doc.vector)) {
109
+ throw new Error(
110
+ 'FileVectorStore.store(): each doc needs id and vector',
111
+ );
112
+ }
113
+ documents[doc.id] = {
114
+ vector: encodeVector(doc.vector),
115
+ content: doc.content || '',
116
+ metadata: doc.metadata || {},
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Remove every document whose metadata.source equals `source`.
123
+ * @param {string} source
124
+ * @returns {number} Number of documents removed
125
+ */
126
+ removeBySource(source) {
127
+ const { documents } = this.load();
128
+ this._revision += 1;
129
+ let removed = 0;
130
+ for (const [id, doc] of Object.entries(documents)) {
131
+ if (doc.metadata?.source === source) {
132
+ delete documents[id];
133
+ removed++;
134
+ }
135
+ }
136
+ return removed;
137
+ }
138
+
139
+ /** Remove every document and the recorded embedder info. */
140
+ clear() {
141
+ this.load();
142
+ this._index = this._emptyIndex();
143
+ this._revision += 1;
144
+ }
145
+
146
+ /** @returns {Array<{id: string, content: string, metadata: object}>} */
147
+ list() {
148
+ return Object.entries(this.load().documents).map(([id, doc]) => ({
149
+ id,
150
+ content: doc.content,
151
+ metadata: doc.metadata,
152
+ }));
153
+ }
154
+
155
+ /** @returns {number} */
156
+ size() {
157
+ return Object.keys(this.load().documents).length;
158
+ }
159
+
160
+ /**
161
+ * Cosine-similarity search.
162
+ *
163
+ * @param {{queryVector: number[], topK?: number}} request
164
+ * @returns {Promise<Array<{id: string, score: number, content: string, metadata: object}>>}
165
+ */
166
+ async retrieve(request = {}) {
167
+ const { queryVector, topK = 5 } = request;
168
+ if (!Array.isArray(queryVector)) {
169
+ throw new Error(
170
+ 'FileVectorStore.retrieve(): queryVector is required. Configure an embedder stage.',
171
+ );
172
+ }
173
+ const info = this.getEmbedderInfo();
174
+ if (info && info.dimensions && info.dimensions !== queryVector.length) {
175
+ throw new Error(
176
+ `Query vector has ${queryVector.length} dimensions but the index was built ` +
177
+ `with ${info.dimensions} (embedder "${info.name}"). Re-run ingest with the ` +
178
+ 'current embedder.',
179
+ );
180
+ }
181
+
182
+ const queryMagnitude = Math.sqrt(
183
+ queryVector.reduce((sum, x) => sum + x * x, 0),
184
+ );
185
+ const results = Object.entries(this.load().documents).map(([id, doc]) => ({
186
+ id,
187
+ score: cosine(queryVector, queryMagnitude, doc.vector),
188
+ content: doc.content,
189
+ metadata: doc.metadata,
190
+ }));
191
+ results.sort((a, b) => b.score - a.score);
192
+ return results.slice(0, topK);
193
+ }
194
+ }
195
+
196
+ // Stored vectors keep ~7 significant digits; more only inflates the file.
197
+ const round = (x) => Number(x.toPrecision(7));
198
+
199
+ /**
200
+ * Mostly-zero vectors (e.g. LocalEmbedder's TF-IDF over a large vocabulary)
201
+ * are stored sparsely as { n, i, v }; dense ones as plain arrays.
202
+ */
203
+ function encodeVector(vector) {
204
+ const indices = [];
205
+ const values = [];
206
+ for (let i = 0; i < vector.length; i++) {
207
+ if (vector[i] !== 0) {
208
+ indices.push(i);
209
+ values.push(round(vector[i]));
210
+ }
211
+ }
212
+ if (indices.length < vector.length / 4) {
213
+ return { n: vector.length, i: indices, v: values };
214
+ }
215
+ return vector.map(round);
216
+ }
217
+
218
+ /** Cosine similarity between a dense query and a stored (dense or sparse) vector. */
219
+ function cosine(query, magQuery, stored) {
220
+ const sparse = !Array.isArray(stored);
221
+ const length = sparse ? stored.n : stored.length;
222
+ if (query.length !== length) return 0;
223
+
224
+ let dot = 0;
225
+ let magStored = 0;
226
+ if (sparse) {
227
+ for (let k = 0; k < stored.i.length; k++) {
228
+ dot += query[stored.i[k]] * stored.v[k];
229
+ magStored += stored.v[k] * stored.v[k];
230
+ }
231
+ } else {
232
+ for (let k = 0; k < length; k++) {
233
+ dot += query[k] * stored[k];
234
+ magStored += stored[k] * stored[k];
235
+ }
236
+ }
237
+ const magnitude = Math.sqrt(magQuery) * Math.sqrt(magStored);
238
+ return magnitude === 0 ? 0 : dot / magnitude;
239
+ }
240
+
241
+ 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,11 +11,27 @@
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
  */
17
32
 
18
- const jwt = require('jsonwebtoken');
33
+ // jsonwebtoken loads on first use (about 90 ms otherwise, on every import).
34
+ const loadJwt = () => require('jsonwebtoken'); // eslint-disable-line global-require
19
35
  const crypto = require('crypto');
20
36
  const { EventEmitter } = require('events');
21
37
  const { logger } = require('../utils/logger');
@@ -91,9 +107,10 @@ class JWTValidator extends EventEmitter {
91
107
  audience: options.audience,
92
108
 
93
109
  // Timing configuration
94
- clockTolerance: options.clockTolerance || 30, // seconds
110
+ // `??` so an explicit 0 is honoured (`||` silently turned 0 into 30).
111
+ clockTolerance: options.clockTolerance ?? 30, // seconds
95
112
  maxTokenAge: options.maxTokenAge || 3600, // 1 hour in seconds
96
- notBeforeTolerance: options.notBeforeTolerance || 30, // seconds
113
+ notBeforeTolerance: options.notBeforeTolerance ?? 30, // seconds
97
114
 
98
115
  // Security options
99
116
  requireJti: options.requireJti !== false, // Require JWT ID by default
@@ -104,6 +121,11 @@ class JWTValidator extends EventEmitter {
104
121
  // JTI tracking for replay protection
105
122
  enableJtiTracking: options.enableJtiTracking !== false,
106
123
  jtiExpirationMs: options.jtiExpirationMs || 7 * 24 * 60 * 60 * 1000, // 7 days
124
+ // Treat external tokens as single-use (default). Set to false to allow
125
+ // normal bearer-token reuse until the token expires.
126
+ singleUseTokens: options.singleUseTokens !== false,
127
+ // Upper bound on tracked JTIs (LRU eviction beyond this).
128
+ maxTrackedJtis: options.maxTrackedJtis ?? 100000,
107
129
 
108
130
  // Additional validation
109
131
  validateClaims: options.validateClaims || {},
@@ -115,6 +137,13 @@ class JWTValidator extends EventEmitter {
115
137
  // Validate configuration
116
138
  this._validateConfig();
117
139
 
140
+ if (
141
+ !Number.isInteger(this.config.maxTrackedJtis) ||
142
+ this.config.maxTrackedJtis < 1
143
+ ) {
144
+ throw new Error('maxTrackedJtis must be a positive integer');
145
+ }
146
+
118
147
  // Initialize JTI tracking
119
148
  if (this.config.enableJtiTracking) {
120
149
  this.usedJtis = new Map(); // jti -> expiration timestamp (for external tokens)
@@ -287,7 +316,7 @@ class JWTValidator extends EventEmitter {
287
316
  }
288
317
 
289
318
  // Sign token with explicit algorithm
290
- const token = jwt.sign(completePayload, signingKey, {
319
+ const token = loadJwt().sign(completePayload, signingKey, {
291
320
  algorithm: this.config.algorithm,
292
321
  // Do not allow algorithm to be overridden
293
322
  header: {
@@ -300,10 +329,9 @@ class JWTValidator extends EventEmitter {
300
329
  // Self-signed tokens are tracked to prevent JTI collisions with external tokens
301
330
  // but they can be verified multiple times (marked as reusable)
302
331
  if (this.config.enableJtiTracking && jti) {
303
- this.selfSignedJtis.add(jti);
304
332
  // Also track in usedJtis with reusable flag to prevent external tokens from using same JTI
305
333
  const expirationTime = (now + expiresIn) * 1000;
306
- this.usedJtis.set(jti, { expirationTime, reusable: true });
334
+ this._trackJti(jti, { expirationTime, reusable: true });
307
335
  }
308
336
 
309
337
  // Update statistics
@@ -406,7 +434,7 @@ class JWTValidator extends EventEmitter {
406
434
  verifyOptions.maxAge = options.maxAge || `${this.config.maxTokenAge}s`;
407
435
  }
408
436
 
409
- const payload = jwt.verify(token, verificationKey, verifyOptions);
437
+ const payload = loadJwt().verify(token, verificationKey, verifyOptions);
410
438
 
411
439
  // Additional claim validation
412
440
  this._validateRequiredClaims(payload);
@@ -417,22 +445,27 @@ class JWTValidator extends EventEmitter {
417
445
  // - External tokens (non-reusable): if race condition occurs, both requests succeed on first use
418
446
  // (acceptable) but subsequent replays are blocked (secure)
419
447
  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
- });
448
+ const jtiEntry = this._getJtiEntry(payload.jti);
449
+ // Self-signed (reusable) tokens are recorded at sign() time
450
+ const isSelfSigned = Boolean(
451
+ jtiEntry && jtiEntry.reusable && this.selfSignedJtis.has(payload.jti),
452
+ );
453
+
454
+ if (!jtiEntry) {
455
+ // First time seeing this JTI. Record it only when single-use
456
+ // checking is on; with singleUseTokens: false bearer tokens may be
457
+ // presented repeatedly until they expire.
458
+ if (this.config.singleUseTokens) {
459
+ this._trackJti(payload.jti, {
460
+ expirationTime: payload.exp * 1000,
461
+ reusable: false,
462
+ });
463
+ }
430
464
  } else {
431
465
  // JTI has been seen before
432
- const jtiEntry = this.usedJtis.get(payload.jti);
433
466
 
434
467
  // Self-signed tokens are reusable - allow multiple verifications
435
- if (jtiEntry.reusable && isSelfSigned) {
468
+ if (isSelfSigned) {
436
469
  // This is valid for refresh flows and load balancer retries
437
470
  } else {
438
471
  // External token being replayed - this is a security violation
@@ -583,6 +616,91 @@ class JWTValidator extends EventEmitter {
583
616
  }
584
617
  }
585
618
 
619
+ /**
620
+ * Expiry time (ms) of a JTI store entry, including clock tolerance.
621
+ * @private
622
+ */
623
+ _jtiExpiry(jtiEntry) {
624
+ // Handle both old format (number) and new format (object)
625
+ const expirationTime =
626
+ typeof jtiEntry === 'number' ? jtiEntry : jtiEntry.expirationTime;
627
+ return expirationTime + this.config.clockTolerance * 1000;
628
+ }
629
+
630
+ /**
631
+ * Remove a JTI from both tracking structures.
632
+ * @private
633
+ */
634
+ _forgetJti(jti) {
635
+ this.usedJtis.delete(jti);
636
+ this.selfSignedJtis.delete(jti);
637
+ }
638
+
639
+ /**
640
+ * Look up a JTI, dropping it if it has expired and refreshing its LRU
641
+ * position otherwise.
642
+ * @private
643
+ */
644
+ _getJtiEntry(jti) {
645
+ const entry = this.usedJtis.get(jti);
646
+ if (entry === undefined) return undefined;
647
+ if (this._jtiExpiry(entry) < Date.now()) {
648
+ this._forgetJti(jti);
649
+ return undefined;
650
+ }
651
+ // Map preserves insertion order: delete + set moves it to most-recent.
652
+ this.usedJtis.delete(jti);
653
+ this.usedJtis.set(jti, entry);
654
+ return entry;
655
+ }
656
+
657
+ /**
658
+ * Record a JTI in the bounded store. When the store is full, expired
659
+ * entries are purged first; if it is still full the least-recently-used
660
+ * entry is evicted.
661
+ * @private
662
+ */
663
+ _trackJti(jti, entry) {
664
+ this._forgetJti(jti);
665
+
666
+ if (this.usedJtis.size >= this.config.maxTrackedJtis) {
667
+ this._purgeExpiredJtis();
668
+ }
669
+
670
+ while (this.usedJtis.size >= this.config.maxTrackedJtis) {
671
+ const oldest = this.usedJtis.keys().next().value;
672
+ this._forgetJti(oldest);
673
+ this.emit('jti_evicted', {
674
+ jti: oldest,
675
+ reason: 'capacity',
676
+ maxTrackedJtis: this.config.maxTrackedJtis,
677
+ timestamp: new Date().toISOString(),
678
+ });
679
+ }
680
+
681
+ this.usedJtis.set(jti, entry);
682
+ if (entry.reusable) {
683
+ this.selfSignedJtis.add(jti);
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Drop every expired JTI from the store.
689
+ * @private
690
+ * @returns {number} Number of entries removed
691
+ */
692
+ _purgeExpiredJtis() {
693
+ const now = Date.now();
694
+ let cleanedCount = 0;
695
+ for (const [jti, jtiEntry] of this.usedJtis.entries()) {
696
+ if (this._jtiExpiry(jtiEntry) < now) {
697
+ this._forgetJti(jti);
698
+ cleanedCount++;
699
+ }
700
+ }
701
+ return cleanedCount;
702
+ }
703
+
586
704
  /**
587
705
  * Start periodic cleanup of expired JTIs
588
706
  * @private
@@ -590,20 +708,7 @@ class JWTValidator extends EventEmitter {
590
708
  _startJtiCleanup() {
591
709
  this.jtiCleanupInterval = setInterval(
592
710
  () => {
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
- }
711
+ const cleanedCount = this._purgeExpiredJtis();
607
712
 
608
713
  if (cleanedCount > 0 && this.config.enableAuditLog) {
609
714
  this.emit('jti_cleanup', {
@@ -630,7 +735,7 @@ class JWTValidator extends EventEmitter {
630
735
  */
631
736
  decode(token) {
632
737
  try {
633
- return jwt.decode(token, { complete: false });
738
+ return loadJwt().decode(token, { complete: false });
634
739
  } catch (error) {
635
740
  throw new Error(`Failed to decode token: ${error.message}`);
636
741
  }
@@ -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