@devilsdev/rag-pipeline-utils 2.4.5 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +431 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +6442 -1355
  9. package/dist/index.d.ts +129 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/testing.cjs +2 -0
  12. package/dist/testing.d.ts +1 -0
  13. package/dist/testing.mjs +5 -0
  14. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  15. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  16. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  17. package/dist/types/agentic/query-planner.d.ts +145 -0
  18. package/dist/types/agentic/self-critique.d.ts +45 -0
  19. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  20. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  21. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  22. package/dist/types/ai/multimodal.d.ts +39 -0
  23. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  24. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  25. package/dist/types/cache/component-keys.d.ts +7 -0
  26. package/dist/types/cache/memory-cache.d.ts +54 -0
  27. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  28. package/dist/types/cache/redis-cache.d.ts +46 -0
  29. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  30. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  31. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  32. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  33. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  34. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  35. package/dist/types/citation/citation-tracker.d.ts +64 -0
  36. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  37. package/dist/types/citation/source-mapper.d.ts +37 -0
  38. package/dist/types/connectors/anthropic-connector.d.ts +97 -0
  39. package/dist/types/connectors/base-connector.d.ts +87 -0
  40. package/dist/types/connectors/cohere-connector.d.ts +94 -0
  41. package/dist/types/connectors/connector-utils.d.ts +110 -0
  42. package/dist/types/connectors/local-embedder.d.ts +95 -0
  43. package/dist/types/connectors/memory-retriever.d.ts +99 -0
  44. package/dist/types/connectors/ollama-connector.d.ts +115 -0
  45. package/dist/types/connectors/openai-connector.d.ts +129 -0
  46. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  47. package/dist/types/core/create-pipeline.d.ts +8 -0
  48. package/dist/types/core/plugin-registry.d.ts +136 -0
  49. package/dist/types/core/stage-contracts.d.ts +36 -0
  50. package/dist/types/core/validate-plugin.d.ts +45 -0
  51. package/dist/types/cost/cost-calculator.d.ts +152 -0
  52. package/dist/types/cost/cost-tracker.d.ts +126 -0
  53. package/dist/types/cost/token-budget.d.ts +110 -0
  54. package/dist/types/dag/core/error-context.d.ts +239 -0
  55. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  56. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  57. package/dist/types/dag/dag-engine.d.ts +156 -0
  58. package/dist/types/dag/dag-node.d.ts +37 -0
  59. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  60. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  61. package/dist/types/debug/execution-tracer.d.ts +57 -0
  62. package/dist/types/debug/trace-inspector.d.ts +31 -0
  63. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  64. package/dist/types/enterprise/data-governance.d.ts +203 -0
  65. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  66. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  67. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  68. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  69. package/dist/types/evaluate/groundedness.d.ts +60 -0
  70. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  71. package/dist/types/evaluate/relevance.d.ts +21 -0
  72. package/dist/types/evaluate/scoring.d.ts +123 -0
  73. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  74. package/dist/types/experimental.d.ts +15 -0
  75. package/dist/types/graph/entity-extractor.d.ts +84 -0
  76. package/dist/types/graph/graph-index.d.ts +49 -0
  77. package/dist/types/graph/graph-retriever.d.ts +81 -0
  78. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  79. package/dist/types/grounding/check-answer.d.ts +101 -0
  80. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  81. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  82. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  83. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  84. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  85. package/dist/types/mcp/mcp-server.d.ts +85 -0
  86. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  87. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  88. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  89. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  90. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  91. package/dist/types/retrieval/file-vector-store.d.ts +99 -0
  92. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  93. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  94. package/dist/types/security/jwt-config.d.ts +80 -0
  95. package/dist/types/security/jwt-validator.d.ts +195 -0
  96. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  97. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  98. package/dist/types/security/validator.d.ts +5 -0
  99. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  100. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  101. package/dist/types/streaming/stream-router.d.ts +88 -0
  102. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  103. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  104. package/dist/types/templates/scaffold.d.ts +55 -0
  105. package/dist/types/templates/template-registry.d.ts +63 -0
  106. package/dist/types/testing.d.ts +168 -0
  107. package/dist/types/utils/embed-query.d.ts +20 -0
  108. package/dist/types/utils/logger.d.ts +50 -0
  109. package/dist/types/utils/rate-limiter.d.ts +66 -0
  110. package/dist/types/utils/retry.d.ts +27 -0
  111. package/dist/types/utils/run-abort.d.ts +34 -0
  112. package/dist/types/utils/secure-logging.d.ts +51 -0
  113. package/dist/types/utils/structured-logger.d.ts +126 -0
  114. package/package.json +72 -33
  115. package/src/adapters/ai-sdk.js +215 -0
  116. package/src/agentic/agentic-pipeline.js +75 -7
  117. package/src/agentic/iterative-retriever.js +47 -10
  118. package/src/agentic/query-planner.js +329 -78
  119. package/src/cache/component-keys.js +38 -0
  120. package/src/cache/memory-cache.js +87 -0
  121. package/src/cache/pipeline-cache.js +260 -0
  122. package/src/cache/redis-cache.js +83 -0
  123. package/src/citation/citation-tracker.js +6 -4
  124. package/src/citation/hallucination-detector.js +2 -2
  125. package/src/citation/source-mapper.js +15 -28
  126. package/src/cli/commands/ai.js +106 -0
  127. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  128. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  129. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  130. package/src/cli/commands/plugin-hub.js +114 -49
  131. package/src/cli/diagnostics/config-checker.js +76 -63
  132. package/src/cli/diagnostics/environment-checker.js +62 -31
  133. package/src/cli/diagnostics/plugin-checker.js +36 -89
  134. package/src/cli/doctor-command.js +39 -5
  135. package/src/cli/enhanced-cli-commands.js +104 -233
  136. package/src/cli/handlers/ai-handler.js +364 -0
  137. package/src/cli/handlers/diagnostics-handler.js +78 -31
  138. package/src/cli/handlers/ingest-handler.js +28 -22
  139. package/src/cli/handlers/init-handler.js +148 -25
  140. package/src/cli/handlers/query-handler.js +203 -25
  141. package/src/cli/handlers/run-handler.js +16 -167
  142. package/src/cli/handlers/serve-handler.js +60 -0
  143. package/src/cli/services/bench-stats.js +54 -0
  144. package/src/cli/services/benchmark.js +161 -0
  145. package/src/cli/services/feedback.js +155 -0
  146. package/src/cli/services/instrument.js +83 -0
  147. package/src/cli/services/rag-service.js +361 -0
  148. package/src/cli/shared/cli-utils.js +24 -7
  149. package/src/cli/shared/config-context.js +21 -0
  150. package/src/cli/wizard/config-builder.js +81 -0
  151. package/src/cli/wizard/init-wizard.js +602 -0
  152. package/src/config/component-factory.js +125 -0
  153. package/src/config/enhanced-ragrc-schema.js +0 -2
  154. package/src/config/load-config.js +29 -10
  155. package/src/config/load-plugin-config.js +46 -2
  156. package/src/config/validate-schema.js +4 -1
  157. package/src/connectors/anthropic-connector.js +115 -57
  158. package/src/connectors/base-connector.js +74 -19
  159. package/src/connectors/cohere-connector.js +108 -34
  160. package/src/connectors/connector-utils.js +302 -0
  161. package/src/connectors/local-embedder.js +63 -5
  162. package/src/connectors/memory-retriever.js +125 -14
  163. package/src/connectors/ollama-connector.js +112 -51
  164. package/src/connectors/openai-connector.js +153 -44
  165. package/src/connectors/rag-prompt.js +88 -0
  166. package/src/core/create-pipeline.js +307 -61
  167. package/src/core/performance/parallel-processor.js +62 -1
  168. package/src/core/plugin-registry.js +125 -26
  169. package/src/core/stage-contracts.js +36 -0
  170. package/src/core/validate-plugin.js +85 -0
  171. package/src/cost/cost-calculator.js +205 -32
  172. package/src/debug/execution-tracer.js +14 -4
  173. package/src/dev/dev-server.js +173 -11
  174. package/src/dev/hot-reload.js +44 -4
  175. package/src/ecosystem/plugin-checks.js +181 -0
  176. package/src/ecosystem/plugin-hub.js +50 -125
  177. package/src/ecosystem/plugin-installer.js +298 -0
  178. package/src/ecosystem/plugin-probe.js +127 -0
  179. package/src/ecosystem/plugin-scanner.js +185 -0
  180. package/src/ecosystem/tar-extract.js +114 -0
  181. package/src/enterprise/audit-logging.js +158 -29
  182. package/src/enterprise/sso-integration.js +716 -228
  183. package/src/evaluate/context-metrics.js +28 -16
  184. package/src/evaluate/evaluator.js +3 -3
  185. package/src/evaluate/faithfulness.js +18 -14
  186. package/src/evaluate/groundedness.js +43 -12
  187. package/src/evaluate/pipeline-evaluator.js +35 -8
  188. package/src/evaluate/scoring.js +251 -3
  189. package/src/evaluate/semantic-similarity.js +79 -0
  190. package/src/experimental.js +71 -0
  191. package/src/grounding/check-answer.js +173 -0
  192. package/src/guardrails/guardrails-pipeline.js +69 -7
  193. package/src/guardrails/injection-detector.js +283 -0
  194. package/src/guardrails/post-generation-guard.js +19 -0
  195. package/src/guardrails/pre-retrieval-guard.js +36 -36
  196. package/src/guardrails/retrieval-guard.js +58 -8
  197. package/src/index.js +58 -1
  198. package/src/loader/csv-loader.js +3 -2
  199. package/src/loader/document-reader.js +91 -0
  200. package/src/loader/html-loader.js +3 -1
  201. package/src/loader/markdown-loader.js +3 -2
  202. package/src/reranker/cascade-reranker.js +3 -0
  203. package/src/reranker/embedding-reranker.js +136 -21
  204. package/src/reranker/llm-reranker.js +79 -12
  205. package/src/reranker/scoring-reranker.js +3 -0
  206. package/src/retrieval/bm25-search.js +12 -6
  207. package/src/retrieval/file-vector-store.js +241 -0
  208. package/src/retrieval/hybrid-retriever.js +22 -19
  209. package/src/security/jwt-config.js +13 -2
  210. package/src/security/jwt-validator.js +139 -34
  211. package/src/security/plugin-sandbox.js +18 -1
  212. package/src/security/plugin-signature-verifier.js +245 -70
  213. package/src/testing.js +254 -0
  214. package/src/utils/deprecate.js +69 -0
  215. package/src/utils/embed-query.js +33 -0
  216. package/src/utils/input-sanitizer.js +19 -4
  217. package/src/utils/logger.js +34 -4
  218. package/src/utils/run-abort.js +102 -0
  219. package/src/ai/federated-learning.js +0 -211
  220. package/src/ai/federation.js +0 -71
  221. package/src/ai/index.js +0 -22
  222. package/src/ai/model-trainer.js +0 -217
  223. package/src/ai/model-training.js +0 -202
  224. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  225. package/src/ai/orchestrator.js +0 -65
  226. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  227. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  228. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  229. package/src/cli/commands/ai-ml/index.js +0 -17
  230. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  231. package/src/cli/commands/ai-ml/shared.js +0 -51
  232. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  233. package/src/cli/commands/ai-ml.js +0 -386
  234. package/src/cli/commands/docs.js +0 -228
  235. package/src/cli/commands/dx.js +0 -538
  236. package/src/cli/commands/wizard.js +0 -627
  237. package/src/cli/input-sanitization-middleware.js +0 -356
  238. package/src/cli/interactive-wizard.js +0 -373
  239. package/src/cli/templates/development.template.json +0 -64
  240. package/src/cli/templates/minimal.template.json +0 -33
  241. package/src/cli/templates/production.template.json +0 -104
  242. package/src/cli/templates/testing.template.json +0 -45
  243. package/src/cli/wizard/configuration.js +0 -148
  244. package/src/cli/wizard/observability.js +0 -79
  245. package/src/cli/wizard/performance.js +0 -76
  246. package/src/cli/wizard/plugin-selection.js +0 -297
  247. package/src/cli/wizard/preview-save.js +0 -172
  248. package/src/cli/wizard/project-setup.js +0 -66
  249. package/src/core/pipeline-factory.js +0 -59
  250. package/src/dag/enhanced-dag-engine.js +0 -894
  251. package/src/dx/index.js +0 -40
  252. package/src/dx/integration-templates.js +0 -662
  253. package/src/dx/performance-profiler.js +0 -968
  254. package/src/dx/realtime-debugger.js +0 -755
  255. package/src/dx/visual-pipeline-builder.js +0 -578
  256. package/src/ingest.js +0 -59
  257. package/src/pipeline/pipeline.js +0 -18
  258. package/src/query.js +0 -117
@@ -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
- // Decode signature
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
- * Verify Ed25519 signature
167
- * @param {Buffer} data - Data that was signed
168
- * @param {Buffer} signature - Signature bytes
169
- * @param {string} publicKeyHex - Public key in hex format
170
- * @returns {Promise<boolean>} Verification result
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
- async _verifyEd25519Signature(data, signature, publicKeyHex) {
173
- try {
174
- // Convert hex public key to buffer
175
- const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
176
-
177
- // Use Node.js crypto for Ed25519 verification
178
- const verify = crypto.createVerify('Ed25519');
179
- verify.update(data);
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
- // Create public key object
182
- const publicKeyObject = crypto.createPublicKey({
183
- key: publicKeyBuffer,
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
- return verify.verify(publicKeyObject, signature);
189
- } catch (error) {
190
- // Fallback to Web Crypto API if available
191
- if (typeof crypto.subtle !== 'undefined') {
192
- try {
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
- return await crypto.subtle.verify(
206
- 'Ed25519',
207
- publicKey,
208
- signature,
209
- data,
210
- );
211
- } catch (webCryptoError) {
212
- throw new Error(`Ed25519 verification failed: ${error.message}`);
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
- throw new Error(`Ed25519 verification failed: ${error.message}`);
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 in hex
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 format
322
- if (!/^[0-9a-fA-F]{64}$/.test(publicKey)) {
323
- throw new Error('Public key must be 64 hex characters (32 bytes)');
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: publicKey.toLowerCase(),
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 };