@devilsdev/rag-pipeline-utils 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +345 -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 +4992 -1242
  9. package/dist/index.d.ts +80 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  12. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  13. package/dist/types/agentic/query-planner.d.ts +145 -0
  14. package/dist/types/agentic/self-critique.d.ts +45 -0
  15. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  16. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  17. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  18. package/dist/types/ai/multimodal.d.ts +39 -0
  19. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  20. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  21. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  22. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  23. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  24. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  25. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  26. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  27. package/dist/types/citation/citation-tracker.d.ts +64 -0
  28. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  29. package/dist/types/citation/source-mapper.d.ts +37 -0
  30. package/dist/types/connectors/anthropic-connector.d.ts +92 -0
  31. package/dist/types/connectors/base-connector.d.ts +87 -0
  32. package/dist/types/connectors/cohere-connector.d.ts +89 -0
  33. package/dist/types/connectors/connector-utils.d.ts +110 -0
  34. package/dist/types/connectors/local-embedder.d.ts +86 -0
  35. package/dist/types/connectors/memory-retriever.d.ts +91 -0
  36. package/dist/types/connectors/ollama-connector.d.ts +110 -0
  37. package/dist/types/connectors/openai-connector.d.ts +114 -0
  38. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  39. package/dist/types/cost/cost-calculator.d.ts +152 -0
  40. package/dist/types/cost/cost-tracker.d.ts +126 -0
  41. package/dist/types/cost/token-budget.d.ts +110 -0
  42. package/dist/types/dag/core/error-context.d.ts +239 -0
  43. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  44. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  45. package/dist/types/dag/dag-engine.d.ts +156 -0
  46. package/dist/types/dag/dag-node.d.ts +37 -0
  47. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  48. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  49. package/dist/types/debug/execution-tracer.d.ts +57 -0
  50. package/dist/types/debug/trace-inspector.d.ts +31 -0
  51. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  52. package/dist/types/enterprise/data-governance.d.ts +203 -0
  53. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  54. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  55. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  56. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  57. package/dist/types/evaluate/groundedness.d.ts +60 -0
  58. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  59. package/dist/types/evaluate/relevance.d.ts +21 -0
  60. package/dist/types/evaluate/scoring.d.ts +123 -0
  61. package/dist/types/experimental.d.ts +15 -0
  62. package/dist/types/graph/entity-extractor.d.ts +84 -0
  63. package/dist/types/graph/graph-index.d.ts +49 -0
  64. package/dist/types/graph/graph-retriever.d.ts +81 -0
  65. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  66. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  67. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  68. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  69. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  70. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  71. package/dist/types/mcp/mcp-server.d.ts +85 -0
  72. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  73. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  74. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  75. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  76. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  77. package/dist/types/retrieval/file-vector-store.d.ts +87 -0
  78. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  79. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  80. package/dist/types/security/jwt-config.d.ts +80 -0
  81. package/dist/types/security/jwt-validator.d.ts +195 -0
  82. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  83. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  84. package/dist/types/security/validator.d.ts +5 -0
  85. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  86. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  87. package/dist/types/streaming/stream-router.d.ts +88 -0
  88. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  89. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  90. package/dist/types/templates/scaffold.d.ts +55 -0
  91. package/dist/types/templates/template-registry.d.ts +63 -0
  92. package/dist/types/utils/embed-query.d.ts +20 -0
  93. package/dist/types/utils/logger.d.ts +50 -0
  94. package/dist/types/utils/rate-limiter.d.ts +66 -0
  95. package/dist/types/utils/retry.d.ts +27 -0
  96. package/dist/types/utils/run-abort.d.ts +34 -0
  97. package/dist/types/utils/secure-logging.d.ts +51 -0
  98. package/dist/types/utils/structured-logger.d.ts +126 -0
  99. package/package.json +53 -29
  100. package/src/agentic/agentic-pipeline.js +75 -7
  101. package/src/agentic/iterative-retriever.js +47 -10
  102. package/src/agentic/query-planner.js +329 -78
  103. package/src/citation/citation-tracker.js +6 -4
  104. package/src/citation/hallucination-detector.js +2 -2
  105. package/src/citation/source-mapper.js +15 -28
  106. package/src/cli/commands/ai.js +101 -0
  107. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  108. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  109. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  110. package/src/cli/commands/plugin-hub.js +96 -36
  111. package/src/cli/diagnostics/config-checker.js +76 -63
  112. package/src/cli/diagnostics/environment-checker.js +62 -31
  113. package/src/cli/diagnostics/plugin-checker.js +36 -89
  114. package/src/cli/doctor-command.js +39 -5
  115. package/src/cli/enhanced-cli-commands.js +72 -217
  116. package/src/cli/handlers/ai-handler.js +364 -0
  117. package/src/cli/handlers/diagnostics-handler.js +78 -31
  118. package/src/cli/handlers/ingest-handler.js +28 -22
  119. package/src/cli/handlers/init-handler.js +148 -25
  120. package/src/cli/handlers/query-handler.js +41 -26
  121. package/src/cli/handlers/run-handler.js +16 -167
  122. package/src/cli/handlers/serve-handler.js +60 -0
  123. package/src/cli/services/bench-stats.js +54 -0
  124. package/src/cli/services/benchmark.js +161 -0
  125. package/src/cli/services/feedback.js +155 -0
  126. package/src/cli/services/rag-service.js +322 -0
  127. package/src/cli/shared/cli-utils.js +24 -7
  128. package/src/cli/shared/config-context.js +21 -0
  129. package/src/cli/wizard/config-builder.js +81 -0
  130. package/src/cli/wizard/init-wizard.js +617 -0
  131. package/src/config/component-factory.js +125 -0
  132. package/src/config/load-config.js +29 -10
  133. package/src/config/load-plugin-config.js +46 -2
  134. package/src/connectors/anthropic-connector.js +93 -55
  135. package/src/connectors/base-connector.js +74 -19
  136. package/src/connectors/cohere-connector.js +88 -32
  137. package/src/connectors/connector-utils.js +302 -0
  138. package/src/connectors/local-embedder.js +43 -5
  139. package/src/connectors/memory-retriever.js +108 -15
  140. package/src/connectors/ollama-connector.js +83 -49
  141. package/src/connectors/openai-connector.js +108 -44
  142. package/src/connectors/rag-prompt.js +88 -0
  143. package/src/core/create-pipeline.js +209 -60
  144. package/src/core/plugin-registry.js +87 -11
  145. package/src/cost/cost-calculator.js +205 -32
  146. package/src/dev/dev-server.js +166 -11
  147. package/src/dev/hot-reload.js +40 -1
  148. package/src/ecosystem/plugin-checks.js +181 -0
  149. package/src/ecosystem/plugin-hub.js +50 -125
  150. package/src/ecosystem/plugin-installer.js +298 -0
  151. package/src/ecosystem/plugin-probe.js +127 -0
  152. package/src/ecosystem/plugin-scanner.js +185 -0
  153. package/src/ecosystem/tar-extract.js +114 -0
  154. package/src/enterprise/audit-logging.js +158 -29
  155. package/src/enterprise/sso-integration.js +715 -228
  156. package/src/evaluate/context-metrics.js +28 -16
  157. package/src/evaluate/evaluator.js +3 -3
  158. package/src/evaluate/faithfulness.js +18 -14
  159. package/src/evaluate/groundedness.js +43 -12
  160. package/src/evaluate/pipeline-evaluator.js +35 -8
  161. package/src/evaluate/scoring.js +251 -3
  162. package/src/experimental.js +71 -0
  163. package/src/guardrails/guardrails-pipeline.js +69 -7
  164. package/src/guardrails/injection-detector.js +283 -0
  165. package/src/guardrails/post-generation-guard.js +19 -0
  166. package/src/guardrails/pre-retrieval-guard.js +36 -36
  167. package/src/guardrails/retrieval-guard.js +58 -8
  168. package/src/index.js +36 -1
  169. package/src/loader/document-reader.js +91 -0
  170. package/src/reranker/cascade-reranker.js +3 -0
  171. package/src/reranker/embedding-reranker.js +136 -21
  172. package/src/reranker/llm-reranker.js +79 -12
  173. package/src/reranker/scoring-reranker.js +3 -0
  174. package/src/retrieval/bm25-search.js +12 -6
  175. package/src/retrieval/file-vector-store.js +212 -0
  176. package/src/retrieval/hybrid-retriever.js +22 -19
  177. package/src/security/jwt-config.js +13 -2
  178. package/src/security/jwt-validator.js +134 -30
  179. package/src/security/plugin-sandbox.js +18 -1
  180. package/src/security/plugin-signature-verifier.js +245 -70
  181. package/src/utils/deprecate.js +69 -0
  182. package/src/utils/embed-query.js +33 -0
  183. package/src/utils/input-sanitizer.js +19 -4
  184. package/src/utils/logger.js +34 -4
  185. package/src/utils/run-abort.js +102 -0
  186. package/src/ai/federated-learning.js +0 -211
  187. package/src/ai/federation.js +0 -71
  188. package/src/ai/index.js +0 -22
  189. package/src/ai/model-trainer.js +0 -217
  190. package/src/ai/model-training.js +0 -202
  191. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  192. package/src/ai/orchestrator.js +0 -65
  193. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  194. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  195. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  196. package/src/cli/commands/ai-ml/index.js +0 -17
  197. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  198. package/src/cli/commands/ai-ml/shared.js +0 -51
  199. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  200. package/src/cli/commands/ai-ml.js +0 -386
  201. package/src/cli/commands/docs.js +0 -228
  202. package/src/cli/commands/dx.js +0 -538
  203. package/src/cli/commands/wizard.js +0 -627
  204. package/src/cli/input-sanitization-middleware.js +0 -356
  205. package/src/cli/interactive-wizard.js +0 -373
  206. package/src/cli/templates/development.template.json +0 -64
  207. package/src/cli/templates/minimal.template.json +0 -33
  208. package/src/cli/templates/production.template.json +0 -104
  209. package/src/cli/templates/testing.template.json +0 -45
  210. package/src/cli/wizard/configuration.js +0 -148
  211. package/src/cli/wizard/observability.js +0 -79
  212. package/src/cli/wizard/performance.js +0 -76
  213. package/src/cli/wizard/plugin-selection.js +0 -297
  214. package/src/cli/wizard/preview-save.js +0 -172
  215. package/src/cli/wizard/project-setup.js +0 -66
  216. package/src/core/pipeline-factory.js +0 -59
  217. package/src/dag/enhanced-dag-engine.js +0 -894
  218. package/src/dx/index.js +0 -40
  219. package/src/dx/integration-templates.js +0 -662
  220. package/src/dx/performance-profiler.js +0 -968
  221. package/src/dx/realtime-debugger.js +0 -755
  222. package/src/dx/visual-pipeline-builder.js +0 -578
  223. package/src/ingest.js +0 -59
  224. package/src/pipeline/pipeline.js +0 -18
  225. package/src/query.js +0 -117
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Static security scan of a plugin's files. It reads source text only and
5
+ * never executes plugin code. It finds capabilities a plugin uses (running
6
+ * processes, evaluating strings as code, network and filesystem writes) so a
7
+ * user can decide whether to trust it; it is not a sandbox or a malware
8
+ * detector, and obfuscated code can evade it.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { relativeWithin } = require('./tar-extract');
14
+
15
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
16
+ const SOURCE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']);
17
+
18
+ /** severity: high blocks installation unless explicitly allowed */
19
+ const RULES = [
20
+ {
21
+ id: 'eval',
22
+ severity: 'high',
23
+ pattern: /\beval\s*\(/,
24
+ message: 'Evaluates strings as code (eval)',
25
+ },
26
+ {
27
+ id: 'new-function',
28
+ severity: 'high',
29
+ pattern: /\bnew\s+Function\s*\(/,
30
+ message: 'Evaluates strings as code (new Function)',
31
+ },
32
+ {
33
+ id: 'child-process',
34
+ severity: 'high',
35
+ pattern: /(?:require\s*\(\s*|from\s+)['"](?:node:)?child_process['"]/,
36
+ message: 'Runs external processes (child_process)',
37
+ },
38
+ {
39
+ id: 'vm',
40
+ severity: 'high',
41
+ pattern: /(?:require\s*\(\s*|from\s+)['"](?:node:)?vm['"]/,
42
+ message: 'Compiles code at runtime (vm)',
43
+ },
44
+ {
45
+ id: 'process-binding',
46
+ severity: 'high',
47
+ pattern: /process\.binding\s*\(/,
48
+ message: 'Uses internal Node bindings (process.binding)',
49
+ },
50
+ {
51
+ id: 'dynamic-require',
52
+ severity: 'medium',
53
+ pattern: /\brequire\s*\(\s*[^'"`\s)]/,
54
+ message: 'Loads modules from a computed path',
55
+ },
56
+ {
57
+ id: 'network',
58
+ severity: 'medium',
59
+ pattern:
60
+ /(?:require\s*\(\s*|from\s+)['"](?:node:)?(?:http|https|net|tls|dgram)['"]|\bfetch\s*\(|['"]axios['"]/,
61
+ message: 'Makes network requests',
62
+ },
63
+ {
64
+ id: 'fs-write',
65
+ severity: 'medium',
66
+ pattern:
67
+ /\b(?:writeFile|appendFile|unlink|rmdir|rm|rename|chmod|createWriteStream)(?:Sync)?\s*\(/,
68
+ message: 'Writes, moves or deletes files',
69
+ },
70
+ {
71
+ id: 'env',
72
+ severity: 'low',
73
+ pattern: /process\.env\b/,
74
+ message: 'Reads environment variables (may include API keys)',
75
+ },
76
+ ];
77
+
78
+ function listFiles(dir) {
79
+ const files = [];
80
+ const walk = (current) => {
81
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
82
+ const full = path.join(current, entry.name);
83
+ if (entry.isSymbolicLink()) continue;
84
+ if (entry.isDirectory()) {
85
+ if (entry.name !== '.git') walk(full);
86
+ } else if (entry.isFile()) {
87
+ files.push(full);
88
+ }
89
+ }
90
+ };
91
+ walk(dir);
92
+ return files;
93
+ }
94
+
95
+ /**
96
+ * @param {string} pluginPath - Plugin directory or single .js file
97
+ * @returns {{
98
+ * findings: Array<{severity: string, rule: string, message: string, file: string, line?: number}>,
99
+ * counts: {high: number, medium: number, low: number},
100
+ * filesScanned: number
101
+ * }}
102
+ */
103
+ function scanPlugin(pluginPath) {
104
+ const root = path.resolve(pluginPath);
105
+ const isDir = fs.statSync(root).isDirectory();
106
+ const baseDir = isDir ? root : path.dirname(root);
107
+ const files = isDir ? listFiles(root) : [root];
108
+ const findings = [];
109
+ let filesScanned = 0;
110
+
111
+ const add = (finding) =>
112
+ findings.push({
113
+ ...finding,
114
+ file:
115
+ relativeWithin(baseDir, finding.file) || path.basename(finding.file),
116
+ });
117
+
118
+ for (const file of files) {
119
+ const ext = path.extname(file).toLowerCase();
120
+ if (ext === '.node') {
121
+ add({
122
+ severity: 'high',
123
+ rule: 'native-addon',
124
+ message: 'Contains a compiled native addon (.node)',
125
+ file,
126
+ });
127
+ continue;
128
+ }
129
+ if (!SOURCE_EXTENSIONS.has(ext)) continue;
130
+ if (fs.statSync(file).size > MAX_FILE_BYTES) {
131
+ add({
132
+ severity: 'medium',
133
+ rule: 'large-file',
134
+ message: 'Source file over 2 MB was not scanned',
135
+ file,
136
+ });
137
+ continue;
138
+ }
139
+ filesScanned++;
140
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
141
+ for (const rule of RULES) {
142
+ const index = lines.findIndex((line) => rule.pattern.test(line));
143
+ if (index !== -1) {
144
+ add({
145
+ severity: rule.severity,
146
+ rule: rule.id,
147
+ message: rule.message,
148
+ file,
149
+ line: index + 1,
150
+ });
151
+ }
152
+ }
153
+ }
154
+
155
+ const manifest = path.join(baseDir, 'package.json');
156
+ if (isDir && fs.existsSync(manifest)) {
157
+ try {
158
+ const scripts =
159
+ JSON.parse(fs.readFileSync(manifest, 'utf8')).scripts || {};
160
+ for (const hook of ['preinstall', 'install', 'postinstall']) {
161
+ if (scripts[hook]) {
162
+ add({
163
+ severity: 'medium',
164
+ rule: 'install-script',
165
+ message: `Declares a "${hook}" script (rag-pipeline never runs it)`,
166
+ file: manifest,
167
+ });
168
+ }
169
+ }
170
+ } catch {
171
+ add({
172
+ severity: 'medium',
173
+ rule: 'bad-manifest',
174
+ message: 'package.json is not valid JSON',
175
+ file: manifest,
176
+ });
177
+ }
178
+ }
179
+
180
+ const counts = { high: 0, medium: 0, low: 0 };
181
+ for (const finding of findings) counts[finding.severity]++;
182
+ return { findings, counts, filesScanned };
183
+ }
184
+
185
+ module.exports = { scanPlugin, RULES };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Minimal .tgz extractor for plugin packages (npm pack format). Extracts
5
+ * regular files and directories only; symlinks, hard links and devices are
6
+ * skipped, and every path must stay inside the destination directory.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const zlib = require('zlib');
12
+
13
+ const BLOCK = 512;
14
+
15
+ /** Separator-agnostic "child is root or inside it" (paths already resolved). */
16
+ function isWithin(root, child) {
17
+ const a = root.replace(/\\/g, '/').replace(/\/$/, '');
18
+ const b = child.replace(/\\/g, '/');
19
+ return b === a || b.startsWith(`${a}/`);
20
+ }
21
+
22
+ function readString(buffer, start, length) {
23
+ const slice = buffer.subarray(start, start + length);
24
+ const end = slice.indexOf(0);
25
+ return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8');
26
+ }
27
+
28
+ function readOctal(buffer, start, length) {
29
+ const text = readString(buffer, start, length).trim();
30
+ return text ? parseInt(text, 8) : 0;
31
+ }
32
+
33
+ /** Parse "<len> path=<value>\n" records from a pax extended header. */
34
+ function parsePaxPath(data) {
35
+ const match = data.toString('utf8').match(/\d+ path=([^\n]*)\n/);
36
+ return match ? match[1] : null;
37
+ }
38
+
39
+ /**
40
+ * Extract a gzipped tarball into destDir.
41
+ *
42
+ * @param {Buffer} archive - .tgz contents
43
+ * @param {string} destDir - Target directory (created if missing)
44
+ * @param {object} [options]
45
+ * @param {number} [options.strip=0] - Leading path components to drop (npm packs use "package/")
46
+ * @param {number} [options.maxBytes=50MB] - Refuse archives that expand beyond this
47
+ * @returns {{files: string[], skipped: string[]}}
48
+ */
49
+ function extractTarGz(archive, destDir, options = {}) {
50
+ const { strip = 0, maxBytes = 50 * 1024 * 1024 } = options;
51
+ const tar = zlib.gunzipSync(archive, { maxOutputLength: maxBytes });
52
+ const root = path.resolve(destDir);
53
+ fs.mkdirSync(root, { recursive: true });
54
+
55
+ const files = [];
56
+ const skipped = [];
57
+ let offset = 0;
58
+ let paxPath = null;
59
+
60
+ while (offset + BLOCK <= tar.length) {
61
+ const header = tar.subarray(offset, offset + BLOCK);
62
+ if (header.every((byte) => byte === 0)) break; // end-of-archive marker
63
+
64
+ const size = readOctal(header, 124, 12);
65
+ const type = String.fromCharCode(header[156] || 48); // '\0' means file
66
+ const prefix = readString(header, 345, 155);
67
+ const headerName = readString(header, 0, 100);
68
+ const name = paxPath || (prefix ? `${prefix}/${headerName}` : headerName);
69
+ const dataStart = offset + BLOCK;
70
+ const data = tar.subarray(dataStart, dataStart + size);
71
+ offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
72
+
73
+ if (type === 'x') {
74
+ paxPath = parsePaxPath(data);
75
+ continue;
76
+ }
77
+ paxPath = null;
78
+ if (type === 'g') continue;
79
+
80
+ const parts = name.split('/').filter((p) => p && p !== '.');
81
+ const relative = parts.slice(strip).join('/');
82
+ if (!relative) continue;
83
+
84
+ const target = path.resolve(root, relative);
85
+ if (!isWithin(root, target)) {
86
+ throw new Error(`Archive entry escapes the destination: ${name}`);
87
+ }
88
+
89
+ if (type === '5') {
90
+ fs.mkdirSync(target, { recursive: true });
91
+ } else if (type === '0' || type === '\0' || type === '7') {
92
+ fs.mkdirSync(path.dirname(target), { recursive: true });
93
+ fs.writeFileSync(target, data);
94
+ files.push(relative);
95
+ } else {
96
+ skipped.push(relative); // links, devices, fifos
97
+ }
98
+ }
99
+ return { files, skipped };
100
+ }
101
+
102
+ module.exports = { extractTarGz, isWithin };
103
+
104
+ /**
105
+ * Path of `child` relative to `root` with "/" separators, or null when it is
106
+ * not inside root. Separator-agnostic like isWithin.
107
+ */
108
+ function relativeWithin(root, child) {
109
+ if (!isWithin(root, child)) return null;
110
+ const a = root.replace(/\\/g, '/').replace(/\/$/, '');
111
+ return child.replace(/\\/g, '/').slice(a.length + 1);
112
+ }
113
+
114
+ module.exports.relativeWithin = relativeWithin;
@@ -13,6 +13,42 @@ const { EventEmitter } = require('events');
13
13
  // eslint-disable-line global-require
14
14
  const { logger } = require('../utils/logger');
15
15
 
16
+ const INTEGRITY_VERSION = 2;
17
+ const ENCRYPTION_VERSION = 2;
18
+ const ENCRYPTION_KEY_ENV = 'RAG_AUDIT_ENCRYPTION_KEY';
19
+
20
+ /**
21
+ * OpenSSL EVP_BytesToKey with MD5, one iteration and no salt: the key/IV
22
+ * derivation used by the removed crypto.createCipher/createDecipher.
23
+ * D_1 = MD5(pass), D_i = MD5(D_{i-1} || pass); key || iv = D_1 || D_2 || ...
24
+ *
25
+ * @param {Buffer|string} password - Passphrase given to createCipher
26
+ * @param {number} keyLen - Key length in bytes
27
+ * @param {number} ivLen - IV length in bytes (12 for GCM)
28
+ * @returns {{key: Buffer, iv: Buffer}}
29
+ */
30
+ function evpBytesToKey(password, keyLen, ivLen) {
31
+ const pass = Buffer.isBuffer(password)
32
+ ? password
33
+ : Buffer.from(String(password), 'utf8');
34
+ const blocks = [];
35
+ let total = 0;
36
+ let prev = Buffer.alloc(0);
37
+ while (total < keyLen + ivLen) {
38
+ prev = crypto
39
+ .createHash('md5')
40
+ .update(Buffer.concat([prev, pass]))
41
+ .digest();
42
+ blocks.push(prev);
43
+ total += prev.length;
44
+ }
45
+ const out = Buffer.concat(blocks);
46
+ return {
47
+ key: out.subarray(0, keyLen),
48
+ iv: out.subarray(keyLen, keyLen + ivLen),
49
+ };
50
+ }
51
+
16
52
  class AuditLogger extends EventEmitter {
17
53
  constructor(_options = {}) {
18
54
  super();
@@ -53,8 +89,7 @@ class AuditLogger extends EventEmitter {
53
89
  };
54
90
 
55
91
  this.logBuffer = [];
56
- this.encryptionKey =
57
- _options.encryptionKey || this._generateEncryptionKey();
92
+ this.encryptionKey = this._resolveEncryptionKey(_options);
58
93
  this.signingKey = _options.signingKey || this._generateSigningKey();
59
94
  this.sequenceNumber = 0;
60
95
 
@@ -355,6 +390,7 @@ class AuditLogger extends EventEmitter {
355
390
  async _writeAuditLog(auditEvent) {
356
391
  // Add sequence number for integrity chain
357
392
  auditEvent.sequence = ++this.sequenceNumber;
393
+ auditEvent.integrityVersion = INTEGRITY_VERSION;
358
394
 
359
395
  // Calculate integrity hash
360
396
  auditEvent.integrity = await this._calculateIntegrityHash(auditEvent);
@@ -384,9 +420,9 @@ class AuditLogger extends EventEmitter {
384
420
 
385
421
  this.emit('audit_logged', {
386
422
  _category: auditEvent._category,
387
- _action: auditEvent._action,
423
+ _action: auditEvent.action,
388
424
  _severity: auditEvent.metadata._severity,
389
- _correlationId: auditEvent.metadata._correlationId,
425
+ _correlationId: auditEvent.metadata.correlationId,
390
426
  });
391
427
 
392
428
  return auditEvent.metadata.correlationId;
@@ -490,12 +526,12 @@ class AuditLogger extends EventEmitter {
490
526
  */
491
527
  async generateComplianceReport(_options) {
492
528
  const {
493
- _tenantId,
494
529
  standard, // GDPR, HIPAA, SOC2, PCI-DSS
495
530
  startDate,
496
531
  endDate,
497
532
  includeEvidence = false,
498
533
  } = _options;
534
+ const _tenantId = _options.tenantId ?? _options._tenantId;
499
535
 
500
536
  const query = {
501
537
  _tenantId,
@@ -566,7 +602,8 @@ class AuditLogger extends EventEmitter {
566
602
  * Verify log integrity
567
603
  */
568
604
  async verifyIntegrity(_options = {}) {
569
- const { startDate, endDate, _tenantId } = _options;
605
+ const { startDate, endDate } = _options;
606
+ const _tenantId = _options.tenantId ?? _options._tenantId;
570
607
 
571
608
  const integrityFile = path.join(
572
609
  this._config.logDir,
@@ -604,7 +641,7 @@ class AuditLogger extends EventEmitter {
604
641
  const expectedHash = await this._calculateIntegrityHash(log);
605
642
  if (log.integrity !== expectedHash) {
606
643
  verification.tamperedLogs.push({
607
- _correlationId: log.metadata._correlationId,
644
+ _correlationId: log.metadata.correlationId,
608
645
  sequence: log.sequence,
609
646
  file: file,
610
647
  });
@@ -616,7 +653,7 @@ class AuditLogger extends EventEmitter {
616
653
  // Verify digital signature if present
617
654
  if (log.signature && !(await this._verifySignature(log))) {
618
655
  verification.tamperedLogs.push({
619
- _correlationId: log.metadata._correlationId,
656
+ _correlationId: log.metadata.correlationId,
620
657
  reason: 'invalid_signature',
621
658
  file: file,
622
659
  });
@@ -630,11 +667,14 @@ class AuditLogger extends EventEmitter {
630
667
 
631
668
  // Private helper methods
632
669
  async _calculateIntegrityHash(auditEvent) {
670
+ // Records written before integrityVersion 2 hashed the non-existent
671
+ // _action/_tenantId fields (always undefined); keep verifying them as-is.
672
+ const isV2 = auditEvent.integrityVersion === INTEGRITY_VERSION;
633
673
  const data = {
634
674
  sequence: auditEvent.sequence,
635
675
  category: auditEvent._category,
636
- action: auditEvent._action,
637
- tenantId: auditEvent._tenantId,
676
+ action: isV2 ? auditEvent.action : auditEvent._action,
677
+ tenantId: isV2 ? auditEvent.tenantId : auditEvent._tenantId,
638
678
  timestamp: auditEvent.metadata.timestamp,
639
679
  };
640
680
 
@@ -649,7 +689,13 @@ class AuditLogger extends EventEmitter {
649
689
 
650
690
  for (const field of sensitiveFields) {
651
691
  if (auditEvent[field]) {
652
- const cipher = crypto.createCipher('aes-256-gcm', this.encryptionKey);
692
+ // Fresh 96-bit IV per field: GCM must never reuse a (key, IV) pair.
693
+ const iv = crypto.randomBytes(12);
694
+ const cipher = crypto.createCipheriv(
695
+ 'aes-256-gcm',
696
+ this._getCipherKey(),
697
+ iv,
698
+ );
653
699
  let encrypted = cipher.update(
654
700
  JSON.stringify(auditEvent[field]),
655
701
  'utf8',
@@ -659,6 +705,8 @@ class AuditLogger extends EventEmitter {
659
705
 
660
706
  auditEvent[field] = {
661
707
  encrypted: true,
708
+ v: ENCRYPTION_VERSION,
709
+ iv: iv.toString('hex'),
662
710
  data: encrypted,
663
711
  authTag: cipher.getAuthTag().toString('hex'),
664
712
  };
@@ -674,10 +722,7 @@ class AuditLogger extends EventEmitter {
674
722
  for (const field of sensitiveFields) {
675
723
  if (auditEvent[field] && auditEvent[field].encrypted) {
676
724
  try {
677
- const decipher = crypto.createDecipher(
678
- 'aes-256-gcm',
679
- this.encryptionKey,
680
- );
725
+ const decipher = this._createDecipher(auditEvent[field]);
681
726
  decipher.setAuthTag(Buffer.from(auditEvent[field].authTag, 'hex'));
682
727
 
683
728
  let decrypted = decipher.update(
@@ -772,8 +817,83 @@ class AuditLogger extends EventEmitter {
772
817
  .substring(0, 16);
773
818
  }
774
819
 
775
- _generateEncryptionKey() {
776
- return crypto.randomBytes(32);
820
+ /**
821
+ * Resolve the field-encryption key.
822
+ *
823
+ * Order: the `encryptionKey` option, then the RAG_AUDIT_ENCRYPTION_KEY
824
+ * environment variable (used as a passphrase, like a string option). A
825
+ * random per-process key is never generated: records written with it
826
+ * could not be decrypted after a restart.
827
+ *
828
+ * If encryption is enabled but no key is configured, encryption is
829
+ * disabled for this instance with a loud warning (an `encryption_disabled`
830
+ * event is also emitted on the next tick). Pass `requireEncryption: true`
831
+ * to throw instead.
832
+ *
833
+ * @param {Object} _options - Constructor options
834
+ * @returns {Buffer|string|null} Key material, or null when unavailable
835
+ * @private
836
+ */
837
+ _resolveEncryptionKey(_options) {
838
+ const configured =
839
+ _options.encryptionKey || process.env[ENCRYPTION_KEY_ENV] || null;
840
+ if (configured || !this._config.compliance.encryption) {
841
+ return configured;
842
+ }
843
+
844
+ const message =
845
+ `AuditLogger: encryption is enabled but no key is configured. Set the ` +
846
+ `encryptionKey option or the ${ENCRYPTION_KEY_ENV} environment variable.`;
847
+
848
+ if (_options.requireEncryption === true) {
849
+ throw new Error(message);
850
+ }
851
+
852
+ logger.warn(
853
+ `[AUDIT] WARNING: ${message} Sensitive audit fields (details, data, ` +
854
+ `request) will be written UNENCRYPTED by this instance. Pass ` +
855
+ `requireEncryption: true to make this an error.`,
856
+ );
857
+ this._config.compliance = {
858
+ ...this._config.compliance,
859
+ encryption: false,
860
+ };
861
+ process.nextTick(() =>
862
+ this.emit('encryption_disabled', { reason: 'no_encryption_key' }),
863
+ );
864
+ return null;
865
+ }
866
+
867
+ /**
868
+ * AES-256 needs exactly 32 key bytes. A 32-byte Buffer is used as-is; any
869
+ * other key (e.g. a passphrase string) is reduced with SHA-256.
870
+ */
871
+ _getCipherKey() {
872
+ const key = this.encryptionKey;
873
+ if (Buffer.isBuffer(key) && key.length === 32) return key;
874
+ return crypto.createHash('sha256').update(key).digest();
875
+ }
876
+
877
+ _createDecipher(encryptedField) {
878
+ if (!this.encryptionKey) {
879
+ throw new Error(
880
+ `No audit encryption key configured (set encryptionKey or ${ENCRYPTION_KEY_ENV})`,
881
+ );
882
+ }
883
+ if (encryptedField.v === ENCRYPTION_VERSION) {
884
+ return crypto.createDecipheriv(
885
+ 'aes-256-gcm',
886
+ this._getCipherKey(),
887
+ Buffer.from(encryptedField.iv, 'hex'),
888
+ );
889
+ }
890
+ // Legacy (v1) records were written with crypto.createCipher, which was
891
+ // removed in Node 22. Re-derive its key and IV exactly as it did and use
892
+ // createDecipheriv so they decrypt on every supported Node version. The
893
+ // passphrase is the raw key material createCipher was given, not the
894
+ // SHA-256-reduced key used for v2 records.
895
+ const { key, iv } = evpBytesToKey(this.encryptionKey, 32, 12);
896
+ return crypto.createDecipheriv('aes-256-gcm', key, iv);
777
897
  }
778
898
 
779
899
  _generateSigningKey() {
@@ -838,8 +958,14 @@ class AuditLogger extends EventEmitter {
838
958
 
839
959
  async _verifySignature(auditEvent) {
840
960
  const { signature, ...eventData } = auditEvent;
961
+ if (typeof signature !== 'string') return false;
841
962
  const expectedSignature = await this._signAuditEvent(eventData);
842
- return signature === expectedSignature;
963
+ const actual = Buffer.from(signature, 'utf8');
964
+ const expected = Buffer.from(expectedSignature, 'utf8');
965
+ // timingSafeEqual requires equal lengths; a length mismatch is not secret
966
+ // (the expected length is fixed by the HMAC algorithm).
967
+ if (actual.length !== expected.length) return false;
968
+ return crypto.timingSafeEqual(actual, expected);
843
969
  }
844
970
 
845
971
  async _getLogFilesInRange(startDate, endDate) {
@@ -873,17 +999,20 @@ class AuditLogger extends EventEmitter {
873
999
  }
874
1000
 
875
1001
  _matchesQuery(log, query) {
876
- if (query.tenantId && log._tenantId !== query._tenantId) return false;
877
- if (query.userId && log._userId !== query._userId) return false;
878
- if (query.category && log._category !== query._category) return false;
879
- if (query.action && log._action !== query._action) return false;
880
- if (query.severity && log.metadata._severity !== query._severity)
881
- return false;
882
- if (
883
- query.correlationId &&
884
- log.metadata._correlationId !== query._correlationId
885
- )
886
- return false;
1002
+ // Accept both plain (tenantId) and underscore (_tenantId) query keys; the
1003
+ // underscore form matches the event input names used by the log* methods.
1004
+ const pick = (key) => query[key] ?? query[`_${key}`];
1005
+ const filters = [
1006
+ [pick('tenantId'), log.tenantId],
1007
+ [pick('userId'), log.userId],
1008
+ [pick('category'), log._category],
1009
+ [pick('action'), log.action],
1010
+ [pick('severity'), log.metadata?._severity],
1011
+ [pick('correlationId'), log.metadata?.correlationId],
1012
+ ];
1013
+ for (const [wanted, actual] of filters) {
1014
+ if (wanted !== undefined && wanted !== actual) return false;
1015
+ }
887
1016
 
888
1017
  const logTime = new Date(log.metadata.timestamp);
889
1018
  if (query.startDate && logTime < new Date(query.startDate)) return false;