@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
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
* Verifies Ed25519 digital signatures for plugin manifests to ensure authenticity
|
|
4
4
|
* Enforces fail-closed verification for production security
|
|
5
5
|
*
|
|
6
|
+
* What is signed: the canonical JSON of the manifest (keys sorted
|
|
7
|
+
* recursively, without `signature`, `signerId` and `signedAt`). To bind the
|
|
8
|
+
* plugin's code to the signature, put a `contentDigest` in the manifest
|
|
9
|
+
* before signing (see computeContentDigest). Because `contentDigest` is part
|
|
10
|
+
* of the signed manifest, changing any covered file invalidates it.
|
|
11
|
+
*
|
|
12
|
+
* Defaults (2.5.0):
|
|
13
|
+
* - The trust store starts EMPTY. Nothing verifies until a signer is added
|
|
14
|
+
* with addTrustedSigner() or a trustedKeysPath file.
|
|
15
|
+
* - If a manifest declares `contentDigest`, the plugin content (files or
|
|
16
|
+
* contents) MUST be supplied to verifyPluginSignature() and must match;
|
|
17
|
+
* otherwise verification fails.
|
|
18
|
+
* - A manifest without `contentDigest` still verifies (manifest-only
|
|
19
|
+
* signatures keep working); pass `requireContentDigest: true` to reject
|
|
20
|
+
* manifests that do not bind the code.
|
|
21
|
+
*
|
|
6
22
|
* @author DevilsDev Team
|
|
7
23
|
* @since 2.1.8
|
|
8
24
|
*/
|
|
@@ -12,6 +28,132 @@ const fs = require('fs');
|
|
|
12
28
|
const path = require('path');
|
|
13
29
|
const { logger } = require('../utils/logger');
|
|
14
30
|
|
|
31
|
+
const DIGEST_PREFIX = 'sha256-';
|
|
32
|
+
const HEX_KEY_RE = /^[0-9a-fA-F]{64}$/;
|
|
33
|
+
const INVALID_KEY_MESSAGE =
|
|
34
|
+
'Public key must be 64 hex characters (32 bytes), or a PEM/DER-encoded Ed25519 public key';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert supported public key encodings to an Ed25519 KeyObject.
|
|
38
|
+
*
|
|
39
|
+
* Accepted: a KeyObject; a 64-character hex string or 32-byte Buffer (raw
|
|
40
|
+
* key); a PEM string ("-----BEGIN PUBLIC KEY-----"); a DER (SPKI) Buffer or
|
|
41
|
+
* base64 string.
|
|
42
|
+
*
|
|
43
|
+
* @param {crypto.KeyObject|Buffer|string} publicKey
|
|
44
|
+
* @returns {crypto.KeyObject}
|
|
45
|
+
*/
|
|
46
|
+
function toEd25519PublicKey(publicKey) {
|
|
47
|
+
let keyObject;
|
|
48
|
+
try {
|
|
49
|
+
if (publicKey instanceof crypto.KeyObject) {
|
|
50
|
+
keyObject =
|
|
51
|
+
publicKey.type === 'private'
|
|
52
|
+
? crypto.createPublicKey(publicKey)
|
|
53
|
+
: publicKey;
|
|
54
|
+
} else if (Buffer.isBuffer(publicKey) && publicKey.length === 32) {
|
|
55
|
+
keyObject = rawToKeyObject(publicKey);
|
|
56
|
+
} else if (Buffer.isBuffer(publicKey)) {
|
|
57
|
+
keyObject = crypto.createPublicKey({
|
|
58
|
+
key: publicKey,
|
|
59
|
+
format: 'der',
|
|
60
|
+
type: 'spki',
|
|
61
|
+
});
|
|
62
|
+
} else if (typeof publicKey === 'string' && HEX_KEY_RE.test(publicKey)) {
|
|
63
|
+
keyObject = rawToKeyObject(Buffer.from(publicKey, 'hex'));
|
|
64
|
+
} else if (
|
|
65
|
+
typeof publicKey === 'string' &&
|
|
66
|
+
publicKey.includes('-----BEGIN')
|
|
67
|
+
) {
|
|
68
|
+
keyObject = crypto.createPublicKey({ key: publicKey, format: 'pem' });
|
|
69
|
+
} else if (typeof publicKey === 'string' && publicKey.length > 0) {
|
|
70
|
+
keyObject = crypto.createPublicKey({
|
|
71
|
+
key: Buffer.from(publicKey, 'base64'),
|
|
72
|
+
format: 'der',
|
|
73
|
+
type: 'spki',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
keyObject = undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
!keyObject ||
|
|
82
|
+
keyObject.type !== 'public' ||
|
|
83
|
+
keyObject.asymmetricKeyType !== 'ed25519'
|
|
84
|
+
) {
|
|
85
|
+
throw new Error(INVALID_KEY_MESSAGE);
|
|
86
|
+
}
|
|
87
|
+
return keyObject;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rawToKeyObject(raw) {
|
|
91
|
+
return crypto.createPublicKey({
|
|
92
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: raw.toString('base64url') },
|
|
93
|
+
format: 'jwk',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Compute the content digest of a plugin.
|
|
99
|
+
*
|
|
100
|
+
* The digest is `sha256-<hex>` of the lines `<relativePath>\0<sha256 of
|
|
101
|
+
* file>\n`, sorted by relative path. Paths are relative to `baseDir` and use
|
|
102
|
+
* forward slashes, so the digest is identical on Windows and POSIX.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} options
|
|
105
|
+
* @param {string[]} [options.files] - File paths (absolute or relative to baseDir)
|
|
106
|
+
* @param {string} [options.baseDir] - Plugin root; files must be inside it
|
|
107
|
+
* @param {Object<string, Buffer|string>} [options.contents] - In-memory
|
|
108
|
+
* files keyed by relative path (alternative or addition to `files`)
|
|
109
|
+
* @returns {string} `sha256-<hex>`
|
|
110
|
+
*/
|
|
111
|
+
function computeContentDigest({ files = [], baseDir, contents = {} } = {}) {
|
|
112
|
+
const root = path.resolve(baseDir || process.cwd());
|
|
113
|
+
const entries = new Map();
|
|
114
|
+
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const abs = path.resolve(root, file);
|
|
117
|
+
const rel = path.relative(root, abs);
|
|
118
|
+
if (
|
|
119
|
+
!rel ||
|
|
120
|
+
rel === '..' ||
|
|
121
|
+
rel.startsWith(`..${path.sep}`) ||
|
|
122
|
+
path.isAbsolute(rel)
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(`Plugin file is outside the plugin directory: ${file}`);
|
|
125
|
+
}
|
|
126
|
+
entries.set(rel.split(path.sep).join('/'), fs.readFileSync(abs));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const [name, value] of Object.entries(contents)) {
|
|
130
|
+
const rel = path.posix.normalize(String(name).split('\\').join('/'));
|
|
131
|
+
if (
|
|
132
|
+
rel === '..' ||
|
|
133
|
+
rel.startsWith('../') ||
|
|
134
|
+
rel === '.' ||
|
|
135
|
+
path.posix.isAbsolute(rel)
|
|
136
|
+
) {
|
|
137
|
+
throw new Error(`Plugin content path is not relative: ${name}`);
|
|
138
|
+
}
|
|
139
|
+
entries.set(rel, Buffer.isBuffer(value) ? value : Buffer.from(value));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (entries.size === 0) {
|
|
143
|
+
throw new Error('No plugin content supplied for digest');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hash = crypto.createHash('sha256');
|
|
147
|
+
for (const rel of [...entries.keys()].sort()) {
|
|
148
|
+
const fileHash = crypto
|
|
149
|
+
.createHash('sha256')
|
|
150
|
+
.update(entries.get(rel))
|
|
151
|
+
.digest('hex');
|
|
152
|
+
hash.update(`${rel}\0${fileHash}\n`, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
return DIGEST_PREFIX + hash.digest('hex');
|
|
155
|
+
}
|
|
156
|
+
|
|
15
157
|
/**
|
|
16
158
|
* Plugin signature verification with Ed25519
|
|
17
159
|
*/
|
|
@@ -22,23 +164,20 @@ class PluginSignatureVerifier {
|
|
|
22
164
|
this.auditLogger =
|
|
23
165
|
options.auditLogger || this._defaultAuditLogger.bind(this);
|
|
24
166
|
this.failClosed = options.failClosed !== false; // Default to fail-closed
|
|
167
|
+
// Opt-in: reject manifests that do not bind the plugin code
|
|
168
|
+
this.requireContentDigest = options.requireContentDigest === true;
|
|
25
169
|
|
|
26
170
|
// Load trusted public keys
|
|
27
171
|
this._loadTrustedKeys(options.trustedKeysPath);
|
|
28
172
|
}
|
|
29
173
|
|
|
30
174
|
/**
|
|
31
|
-
* Load trusted public keys from configuration
|
|
175
|
+
* Load trusted public keys from configuration.
|
|
176
|
+
* Without a keys file the trust store stays empty (no built-in keys).
|
|
32
177
|
* @param {string} keysPath - Path to trusted keys file
|
|
33
178
|
*/
|
|
34
179
|
_loadTrustedKeys(keysPath) {
|
|
35
180
|
if (!keysPath) {
|
|
36
|
-
// Default trusted keys for the DevilsDev organization
|
|
37
|
-
this.trustStore.set('devilsdev-official', {
|
|
38
|
-
publicKey: 'your-ed25519-public-key-here',
|
|
39
|
-
name: 'DevilsDev Official Plugins',
|
|
40
|
-
verified: true,
|
|
41
|
-
});
|
|
42
181
|
return;
|
|
43
182
|
}
|
|
44
183
|
|
|
@@ -49,6 +188,8 @@ class PluginSignatureVerifier {
|
|
|
49
188
|
|
|
50
189
|
for (const [keyId, keyInfo] of Object.entries(trustedKeys)) {
|
|
51
190
|
if (keyInfo.publicKey && keyInfo.name) {
|
|
191
|
+
// Reject unusable keys up front instead of at verification time
|
|
192
|
+
toEd25519PublicKey(keyInfo.publicKey);
|
|
52
193
|
this.trustStore.set(keyId, keyInfo);
|
|
53
194
|
}
|
|
54
195
|
}
|
|
@@ -57,6 +198,9 @@ class PluginSignatureVerifier {
|
|
|
57
198
|
if (this.failClosed) {
|
|
58
199
|
throw new Error(`Failed to load trusted keys: ${error.message}`);
|
|
59
200
|
}
|
|
201
|
+
logger.warn(
|
|
202
|
+
`[PLUGIN_SIGNATURE] Failed to load trusted keys: ${error.message}`,
|
|
203
|
+
);
|
|
60
204
|
}
|
|
61
205
|
}
|
|
62
206
|
|
|
@@ -65,11 +209,15 @@ class PluginSignatureVerifier {
|
|
|
65
209
|
* @param {object} manifest - Plugin manifest
|
|
66
210
|
* @param {string} signature - Base64 encoded signature
|
|
67
211
|
* @param {string} signerId - Signer identifier
|
|
212
|
+
* @param {object} [content] - Plugin content to check against
|
|
213
|
+
* manifest.contentDigest: `{ files, baseDir, contents }` (see
|
|
214
|
+
* computeContentDigest)
|
|
68
215
|
* @returns {Promise<object>} Verification result
|
|
69
216
|
*/
|
|
70
|
-
async verifyPluginSignature(manifest, signature, signerId) {
|
|
217
|
+
async verifyPluginSignature(manifest, signature, signerId, content = null) {
|
|
71
218
|
const verificationResult = {
|
|
72
219
|
verified: false,
|
|
220
|
+
contentVerified: false,
|
|
73
221
|
signerId,
|
|
74
222
|
timestamp: new Date().toISOString(),
|
|
75
223
|
error: null,
|
|
@@ -115,15 +263,8 @@ class PluginSignatureVerifier {
|
|
|
115
263
|
'utf8',
|
|
116
264
|
);
|
|
117
265
|
|
|
118
|
-
|
|
119
|
-
let signatureBuffer;
|
|
120
|
-
try {
|
|
121
|
-
signatureBuffer = Buffer.from(signature, 'base64');
|
|
122
|
-
} catch (error) {
|
|
123
|
-
throw new Error(`Invalid signature encoding: ${error.message}`);
|
|
124
|
-
}
|
|
266
|
+
const signatureBuffer = Buffer.from(signature, 'base64');
|
|
125
267
|
|
|
126
|
-
// Verify signature using Web Crypto API or Node.js crypto
|
|
127
268
|
const isValid = await this._verifyEd25519Signature(
|
|
128
269
|
manifestData,
|
|
129
270
|
signatureBuffer,
|
|
@@ -134,14 +275,22 @@ class PluginSignatureVerifier {
|
|
|
134
275
|
throw new Error('Signature verification failed');
|
|
135
276
|
}
|
|
136
277
|
|
|
278
|
+
// The signature covers contentDigest; now check the code matches it.
|
|
279
|
+
verificationResult.contentVerified = this._verifyContentDigest(
|
|
280
|
+
canonicalManifest,
|
|
281
|
+
content,
|
|
282
|
+
);
|
|
283
|
+
|
|
137
284
|
verificationResult.verified = true;
|
|
138
285
|
verificationResult.auditTrail.push({
|
|
139
286
|
action: 'signature_verified',
|
|
140
287
|
signerId,
|
|
141
288
|
timestamp: new Date().toISOString(),
|
|
142
289
|
manifestHash: this._hashManifest(canonicalManifest),
|
|
290
|
+
contentVerified: verificationResult.contentVerified,
|
|
143
291
|
});
|
|
144
292
|
} catch (error) {
|
|
293
|
+
verificationResult.verified = false;
|
|
145
294
|
verificationResult.error = error.message;
|
|
146
295
|
verificationResult.auditTrail.push({
|
|
147
296
|
action: 'verification_failed',
|
|
@@ -163,58 +312,66 @@ class PluginSignatureVerifier {
|
|
|
163
312
|
}
|
|
164
313
|
|
|
165
314
|
/**
|
|
166
|
-
*
|
|
167
|
-
* @param {
|
|
168
|
-
* @param {
|
|
169
|
-
* @
|
|
170
|
-
* @
|
|
315
|
+
* Check the supplied plugin content against the signed contentDigest.
|
|
316
|
+
* @param {object} canonicalManifest - Signed manifest
|
|
317
|
+
* @param {object|null} content - `{ files, baseDir, contents }`
|
|
318
|
+
* @returns {boolean} true when the content digest was checked and matched
|
|
319
|
+
* @private
|
|
171
320
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
321
|
+
_verifyContentDigest(canonicalManifest, content) {
|
|
322
|
+
const declared = canonicalManifest.contentDigest;
|
|
323
|
+
const hasContent =
|
|
324
|
+
content &&
|
|
325
|
+
((Array.isArray(content.files) && content.files.length > 0) ||
|
|
326
|
+
(content.contents && Object.keys(content.contents).length > 0));
|
|
327
|
+
|
|
328
|
+
if (declared === undefined || declared === null) {
|
|
329
|
+
if (this.requireContentDigest) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
'Manifest has no contentDigest; the signature does not cover the plugin code',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
180
336
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
format: 'der',
|
|
185
|
-
type: 'spki',
|
|
186
|
-
});
|
|
337
|
+
if (typeof declared !== 'string' || !declared.startsWith(DIGEST_PREFIX)) {
|
|
338
|
+
throw new Error('Invalid contentDigest: expected "sha256-<hex>"');
|
|
339
|
+
}
|
|
187
340
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
|
|
194
|
-
const publicKey = await crypto.subtle.importKey(
|
|
195
|
-
'raw',
|
|
196
|
-
publicKeyBuffer,
|
|
197
|
-
{
|
|
198
|
-
name: 'Ed25519',
|
|
199
|
-
namedCurve: 'Ed25519',
|
|
200
|
-
},
|
|
201
|
-
false,
|
|
202
|
-
['verify'],
|
|
203
|
-
);
|
|
341
|
+
if (!hasContent) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
'Manifest declares a contentDigest but no plugin content was supplied to verify it',
|
|
344
|
+
);
|
|
345
|
+
}
|
|
204
346
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
347
|
+
const actual = Buffer.from(computeContentDigest(content), 'utf8');
|
|
348
|
+
const expected = Buffer.from(declared.toLowerCase(), 'utf8');
|
|
349
|
+
if (
|
|
350
|
+
actual.length !== expected.length ||
|
|
351
|
+
!crypto.timingSafeEqual(actual, expected)
|
|
352
|
+
) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
'Plugin content digest mismatch: plugin code does not match the signed manifest',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
215
359
|
|
|
216
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Verify Ed25519 signature
|
|
362
|
+
* @param {Buffer} data - Data that was signed
|
|
363
|
+
* @param {Buffer} signature - Signature bytes
|
|
364
|
+
* @param {crypto.KeyObject|Buffer|string} publicKey - Ed25519 public key
|
|
365
|
+
* (raw hex, PEM, DER or KeyObject)
|
|
366
|
+
* @returns {Promise<boolean>} Verification result
|
|
367
|
+
*/
|
|
368
|
+
async _verifyEd25519Signature(data, signature, publicKey) {
|
|
369
|
+
const keyObject = toEd25519PublicKey(publicKey);
|
|
370
|
+
if (!Buffer.isBuffer(signature) || signature.length !== 64) {
|
|
371
|
+
return false;
|
|
217
372
|
}
|
|
373
|
+
// Ed25519 takes no digest algorithm: pass null.
|
|
374
|
+
return crypto.verify(null, data, keyObject, signature);
|
|
218
375
|
}
|
|
219
376
|
|
|
220
377
|
/**
|
|
@@ -233,6 +390,18 @@ class PluginSignatureVerifier {
|
|
|
233
390
|
return this._sortObjectKeys(canonical);
|
|
234
391
|
}
|
|
235
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Bytes that a signer must sign for a manifest (canonical JSON).
|
|
395
|
+
* @param {object} manifest - Plugin manifest
|
|
396
|
+
* @returns {Buffer}
|
|
397
|
+
*/
|
|
398
|
+
getSigningPayload(manifest) {
|
|
399
|
+
return Buffer.from(
|
|
400
|
+
JSON.stringify(this._canonicalizeManifest(manifest)),
|
|
401
|
+
'utf8',
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
236
405
|
/**
|
|
237
406
|
* Sort object keys recursively
|
|
238
407
|
* @param {any} obj - Object to sort
|
|
@@ -276,10 +445,11 @@ class PluginSignatureVerifier {
|
|
|
276
445
|
const auditEntry = {
|
|
277
446
|
timestamp: result.timestamp,
|
|
278
447
|
action: 'plugin_signature_verification',
|
|
279
|
-
pluginId: manifest.name || 'unknown',
|
|
280
|
-
pluginVersion: manifest.version || 'unknown',
|
|
448
|
+
pluginId: (manifest && manifest.name) || 'unknown',
|
|
449
|
+
pluginVersion: (manifest && manifest.version) || 'unknown',
|
|
281
450
|
signerId: result.signerId,
|
|
282
451
|
verified: result.verified,
|
|
452
|
+
contentVerified: result.contentVerified,
|
|
283
453
|
error: result.error,
|
|
284
454
|
auditTrail: result.auditTrail,
|
|
285
455
|
};
|
|
@@ -309,7 +479,8 @@ class PluginSignatureVerifier {
|
|
|
309
479
|
/**
|
|
310
480
|
* Add trusted signer
|
|
311
481
|
* @param {string} signerId - Signer identifier
|
|
312
|
-
* @param {string} publicKey - Ed25519 public key
|
|
482
|
+
* @param {string|Buffer|crypto.KeyObject} publicKey - Ed25519 public key:
|
|
483
|
+
* 64 hex characters (raw 32 bytes), PEM, DER (SPKI) or a KeyObject
|
|
313
484
|
* @param {string} name - Human-readable name
|
|
314
485
|
* @param {object} metadata - Additional metadata
|
|
315
486
|
*/
|
|
@@ -318,13 +489,15 @@ class PluginSignatureVerifier {
|
|
|
318
489
|
throw new Error('signerId, publicKey, and name are required');
|
|
319
490
|
}
|
|
320
491
|
|
|
321
|
-
// Validate public key
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
492
|
+
// Validate the key is a usable Ed25519 public key
|
|
493
|
+
const keyObject = toEd25519PublicKey(publicKey);
|
|
494
|
+
const storedKey =
|
|
495
|
+
typeof publicKey === 'string' && HEX_KEY_RE.test(publicKey)
|
|
496
|
+
? publicKey.toLowerCase()
|
|
497
|
+
: keyObject.export({ format: 'pem', type: 'spki' });
|
|
325
498
|
|
|
326
499
|
this.trustStore.set(signerId, {
|
|
327
|
-
publicKey:
|
|
500
|
+
publicKey: storedKey,
|
|
328
501
|
name,
|
|
329
502
|
verified: metadata.verified || false,
|
|
330
503
|
addedAt: new Date().toISOString(),
|
|
@@ -409,4 +582,6 @@ function createPluginVerifier(options = {}) {
|
|
|
409
582
|
module.exports = {
|
|
410
583
|
PluginSignatureVerifier,
|
|
411
584
|
createPluginVerifier,
|
|
585
|
+
computeContentDigest,
|
|
586
|
+
toEd25519PublicKey,
|
|
412
587
|
};
|
package/src/testing.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @devilsdev/rag-pipeline-utils/testing
|
|
5
|
+
*
|
|
6
|
+
* Deterministic, offline stand-ins for testing code that uses this library:
|
|
7
|
+
* no network, no API keys, the same output on every run.
|
|
8
|
+
*
|
|
9
|
+
* - HashEmbedder: bag-of-words vectors from hashed words.
|
|
10
|
+
* - EchoLLM: answers with the top retrieved document (or an answer you set)
|
|
11
|
+
* and records every call.
|
|
12
|
+
* - fixtures: a few short policy documents.
|
|
13
|
+
* - createTestPipeline(): a ready, indexed pipeline built from the above.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { createRagPipeline } = require('./core/create-pipeline');
|
|
17
|
+
const { MemoryRetriever } = require('./connectors/memory-retriever');
|
|
18
|
+
|
|
19
|
+
function throwIfAborted(signal) {
|
|
20
|
+
if (signal && signal.aborted) {
|
|
21
|
+
throw signal.reason || new Error('Aborted');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deterministic embedder: hashed bag of words, L2-normalised. Texts that
|
|
27
|
+
* share words get similar vectors, so retrieval ranks sensibly in tests.
|
|
28
|
+
*/
|
|
29
|
+
class HashEmbedder {
|
|
30
|
+
/**
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {number} [options.dimensions=256] - Vector length
|
|
33
|
+
*/
|
|
34
|
+
constructor({ dimensions = 256 } = {}) {
|
|
35
|
+
if (!Number.isInteger(dimensions) || dimensions < 1) {
|
|
36
|
+
throw new Error('dimensions must be a positive integer');
|
|
37
|
+
}
|
|
38
|
+
this.dimensions = dimensions;
|
|
39
|
+
this.metadata = {
|
|
40
|
+
name: 'hash-embedder',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
type: 'embedder',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Cache identity for createRagPipeline({ cache }).
|
|
48
|
+
* @param {string} role
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
getCacheKey(role) {
|
|
52
|
+
return role === 'embedder' ? `hash-embedder|${this.dimensions}` : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @private */
|
|
56
|
+
_vector(text) {
|
|
57
|
+
const vector = new Array(this.dimensions).fill(0);
|
|
58
|
+
for (const word of String(text)
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.match(/[\p{L}\p{N}]+/gu) || []) {
|
|
61
|
+
let hash = 2166136261;
|
|
62
|
+
for (const ch of word) {
|
|
63
|
+
hash ^= ch.codePointAt(0);
|
|
64
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
65
|
+
}
|
|
66
|
+
// FNV-1a's low bits are weak (e.g. "above" and "vacation" agree in
|
|
67
|
+
// their low 10 bits), so mix them (MurmurHash3 fmix32) before taking
|
|
68
|
+
// the bucket.
|
|
69
|
+
hash ^= hash >>> 16;
|
|
70
|
+
hash = Math.imul(hash, 0x85ebca6b);
|
|
71
|
+
hash ^= hash >>> 13;
|
|
72
|
+
hash = Math.imul(hash, 0xc2b2ae35);
|
|
73
|
+
hash ^= hash >>> 16;
|
|
74
|
+
hash >>>= 0;
|
|
75
|
+
// Signed feature hashing: the top bit picks the sign, so colliding
|
|
76
|
+
// words cancel out on average instead of adding up.
|
|
77
|
+
vector[hash % this.dimensions] += hash & 0x80000000 ? -1 : 1;
|
|
78
|
+
}
|
|
79
|
+
const norm = Math.hypot(...vector);
|
|
80
|
+
return norm > 0 ? vector.map((v) => v / norm) : vector;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} text
|
|
85
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
86
|
+
* @returns {Promise<number[]>}
|
|
87
|
+
*/
|
|
88
|
+
async embed(text, { signal } = {}) {
|
|
89
|
+
throwIfAborted(signal);
|
|
90
|
+
return this._vector(text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Same as embed().
|
|
95
|
+
* @param {string} text
|
|
96
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
97
|
+
* @returns {Promise<number[]>}
|
|
98
|
+
*/
|
|
99
|
+
async embedQuery(text, options) {
|
|
100
|
+
return this.embed(text, options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string[]} texts
|
|
105
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
106
|
+
* @returns {Promise<number[][]>}
|
|
107
|
+
*/
|
|
108
|
+
async embedBatch(texts, { signal } = {}) {
|
|
109
|
+
throwIfAborted(signal);
|
|
110
|
+
return texts.map((t) => this._vector(t));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* LLM stand-in. Answers with `answer` (a string, or a function of the query
|
|
116
|
+
* and documents), or else with the top document's content. Every call is
|
|
117
|
+
* recorded in `calls`, so tests can assert on what the pipeline sent.
|
|
118
|
+
*/
|
|
119
|
+
class EchoLLM {
|
|
120
|
+
/**
|
|
121
|
+
* @param {Object} [options]
|
|
122
|
+
* @param {string|function(string, Array<Object>): string} [options.answer]
|
|
123
|
+
* @param {string} [options.fallback='No information found.'] - Answer when
|
|
124
|
+
* there are no documents and no `answer`
|
|
125
|
+
*/
|
|
126
|
+
constructor({ answer, fallback = 'No information found.' } = {}) {
|
|
127
|
+
this.answer = answer;
|
|
128
|
+
this.fallback = fallback;
|
|
129
|
+
/** @type {Array<{query: string, docs: Array<Object>, options: Object}>} */
|
|
130
|
+
this.calls = [];
|
|
131
|
+
this.metadata = { name: 'echo-llm', version: '1.0.0', type: 'llm' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Cache identity for createRagPipeline({ cache }); none when `answer` is a
|
|
136
|
+
* function, since its output cannot be identified.
|
|
137
|
+
* @param {string} role
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
getCacheKey(role) {
|
|
141
|
+
if (role !== 'llm' || typeof this.answer === 'function') return null;
|
|
142
|
+
return `echo-llm|${JSON.stringify([this.answer ?? null, this.fallback])}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @private */
|
|
146
|
+
_answer(query, docs) {
|
|
147
|
+
if (typeof this.answer === 'function')
|
|
148
|
+
return String(this.answer(query, docs));
|
|
149
|
+
if (typeof this.answer === 'string') return this.answer;
|
|
150
|
+
const top = docs && docs[0];
|
|
151
|
+
if (!top) return this.fallback;
|
|
152
|
+
return typeof top === 'string'
|
|
153
|
+
? top
|
|
154
|
+
: String(top.content ?? top.text ?? '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} query
|
|
159
|
+
* @param {Array<Object>} [docs]
|
|
160
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
161
|
+
* @returns {Promise<string>}
|
|
162
|
+
*/
|
|
163
|
+
async generate(query, docs = [], options = {}) {
|
|
164
|
+
throwIfAborted(options.signal);
|
|
165
|
+
this.calls.push({ query, docs, options });
|
|
166
|
+
return this._answer(query, docs);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Streams the same answer word by word, then a done chunk.
|
|
171
|
+
* @param {string} query
|
|
172
|
+
* @param {Array<Object>} [docs]
|
|
173
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
174
|
+
* @yields {{ token: string, done: boolean }}
|
|
175
|
+
*/
|
|
176
|
+
async *generateStream(query, docs = [], options = {}) {
|
|
177
|
+
throwIfAborted(options.signal);
|
|
178
|
+
this.calls.push({ query, docs, options, stream: true });
|
|
179
|
+
for (const token of this._answer(query, docs).split(/(?<= )/)) {
|
|
180
|
+
throwIfAborted(options.signal);
|
|
181
|
+
yield { token, done: false };
|
|
182
|
+
}
|
|
183
|
+
yield { token: '', done: true };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Short documents for tests and examples. */
|
|
188
|
+
const fixtures = Object.freeze({
|
|
189
|
+
documents: Object.freeze([
|
|
190
|
+
Object.freeze({
|
|
191
|
+
id: 'vacation',
|
|
192
|
+
content:
|
|
193
|
+
'Full-time employees receive twenty days of paid vacation per year. Unused days carry over to March.',
|
|
194
|
+
metadata: Object.freeze({ source: 'vacation.md' }),
|
|
195
|
+
}),
|
|
196
|
+
Object.freeze({
|
|
197
|
+
id: 'remote-work',
|
|
198
|
+
content:
|
|
199
|
+
'Employees may work remotely up to three days per week with manager approval.',
|
|
200
|
+
metadata: Object.freeze({ source: 'remote-work.md' }),
|
|
201
|
+
}),
|
|
202
|
+
Object.freeze({
|
|
203
|
+
id: 'expenses',
|
|
204
|
+
content:
|
|
205
|
+
'Submit receipts within thirty days. Travel above 500 EUR needs pre-approval from finance.',
|
|
206
|
+
metadata: Object.freeze({ source: 'expenses.md' }),
|
|
207
|
+
}),
|
|
208
|
+
]),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A pipeline with HashEmbedder, a MemoryRetriever already holding
|
|
213
|
+
* `documents`, and EchoLLM (or the llm you pass).
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const { createTestPipeline } = require('@devilsdev/rag-pipeline-utils/testing');
|
|
217
|
+
* const { pipeline, llm } = await createTestPipeline();
|
|
218
|
+
* const result = await pipeline.run({ query: 'How many vacation days?' });
|
|
219
|
+
* // result.results[0].id === 'vacation'; llm.calls.length === 1
|
|
220
|
+
*
|
|
221
|
+
* @param {Object} [options]
|
|
222
|
+
* @param {Array<{id: string, content: string, metadata?: Object}>} [options.documents=fixtures.documents]
|
|
223
|
+
* @param {Object} [options.embedder] - Defaults to a new HashEmbedder
|
|
224
|
+
* @param {Object|null} [options.llm] - Defaults to a new EchoLLM; null for retrieval only
|
|
225
|
+
* @param {Object} [options.pipeline] - Extra createRagPipeline options (e.g. reranker)
|
|
226
|
+
* @returns {Promise<{ pipeline: Object, embedder: Object, retriever: MemoryRetriever, llm: Object|null }>}
|
|
227
|
+
*/
|
|
228
|
+
async function createTestPipeline({
|
|
229
|
+
documents = fixtures.documents,
|
|
230
|
+
embedder = new HashEmbedder(),
|
|
231
|
+
llm = new EchoLLM(),
|
|
232
|
+
pipeline: pipelineOptions = {},
|
|
233
|
+
} = {}) {
|
|
234
|
+
const retriever = new MemoryRetriever();
|
|
235
|
+
const vectors = await Promise.all(
|
|
236
|
+
documents.map((d) => embedder.embed(d.content)),
|
|
237
|
+
);
|
|
238
|
+
await retriever.store(
|
|
239
|
+
documents.map((d, i) => ({
|
|
240
|
+
...d,
|
|
241
|
+
metadata: { ...(d.metadata || {}) },
|
|
242
|
+
vector: vectors[i],
|
|
243
|
+
})),
|
|
244
|
+
);
|
|
245
|
+
const pipeline = createRagPipeline({
|
|
246
|
+
...pipelineOptions,
|
|
247
|
+
embedder,
|
|
248
|
+
retriever,
|
|
249
|
+
...(llm && { llm }),
|
|
250
|
+
});
|
|
251
|
+
return { pipeline, embedder, retriever, llm: llm || null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { HashEmbedder, EchoLLM, fixtures, createTestPipeline };
|