@devilsdev/rag-pipeline-utils 2.4.5 → 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.
- package/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +345 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +4992 -1242
- package/dist/index.d.ts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
- package/dist/types/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +92 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +89 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +86 -0
- package/dist/types/connectors/memory-retriever.d.ts +91 -0
- package/dist/types/connectors/ollama-connector.d.ts +110 -0
- package/dist/types/connectors/openai-connector.d.ts +114 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +87 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +53 -29
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- package/src/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +101 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +96 -36
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +72 -217
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +41 -26
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/rag-service.js +322 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +617 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/connectors/anthropic-connector.js +93 -55
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +88 -32
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +43 -5
- package/src/connectors/memory-retriever.js +108 -15
- package/src/connectors/ollama-connector.js +83 -49
- package/src/connectors/openai-connector.js +108 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +209 -60
- package/src/core/plugin-registry.js +87 -11
- package/src/cost/cost-calculator.js +205 -32
- package/src/dev/dev-server.js +166 -11
- package/src/dev/hot-reload.js +40 -1
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +715 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/experimental.js +71 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +36 -1
- package/src/loader/document-reader.js +91 -0
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +212 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +134 -30
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- 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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|