@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.
- package/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +431 -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 +6442 -1355
- package/dist/index.d.ts +129 -2
- package/dist/index.mjs +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.mjs +5 -0
- package/dist/types/adapters/ai-sdk.d.ts +53 -0
- 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/cache/component-keys.d.ts +7 -0
- package/dist/types/cache/memory-cache.d.ts +54 -0
- package/dist/types/cache/pipeline-cache.d.ts +66 -0
- package/dist/types/cache/redis-cache.d.ts +46 -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 +97 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +94 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +95 -0
- package/dist/types/connectors/memory-retriever.d.ts +99 -0
- package/dist/types/connectors/ollama-connector.d.ts +115 -0
- package/dist/types/connectors/openai-connector.d.ts +129 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/core/create-pipeline.d.ts +8 -0
- package/dist/types/core/plugin-registry.d.ts +136 -0
- package/dist/types/core/stage-contracts.d.ts +36 -0
- package/dist/types/core/validate-plugin.d.ts +45 -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/evaluate/semantic-similarity.d.ts +14 -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/grounding/check-answer.d.ts +101 -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 +99 -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/testing.d.ts +168 -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 +72 -33
- package/src/adapters/ai-sdk.js +215 -0
- 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/cache/component-keys.js +38 -0
- package/src/cache/memory-cache.js +87 -0
- package/src/cache/pipeline-cache.js +260 -0
- package/src/cache/redis-cache.js +83 -0
- 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 +106 -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 +114 -49
- 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 +104 -233
- 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 +203 -25
- 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/instrument.js +83 -0
- package/src/cli/services/rag-service.js +361 -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 +602 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/enhanced-ragrc-schema.js +0 -2
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/config/validate-schema.js +4 -1
- package/src/connectors/anthropic-connector.js +115 -57
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +108 -34
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +63 -5
- package/src/connectors/memory-retriever.js +125 -14
- package/src/connectors/ollama-connector.js +112 -51
- package/src/connectors/openai-connector.js +153 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +307 -61
- package/src/core/performance/parallel-processor.js +62 -1
- package/src/core/plugin-registry.js +125 -26
- package/src/core/stage-contracts.js +36 -0
- package/src/core/validate-plugin.js +85 -0
- package/src/cost/cost-calculator.js +205 -32
- package/src/debug/execution-tracer.js +14 -4
- package/src/dev/dev-server.js +173 -11
- package/src/dev/hot-reload.js +44 -4
- 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 +716 -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/evaluate/semantic-similarity.js +79 -0
- package/src/experimental.js +71 -0
- package/src/grounding/check-answer.js +173 -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 +58 -1
- package/src/loader/csv-loader.js +3 -2
- package/src/loader/document-reader.js +91 -0
- package/src/loader/html-loader.js +3 -1
- package/src/loader/markdown-loader.js +3 -2
- 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 +241 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +139 -34
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/testing.js +254 -0
- 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,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
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|