@devilsdev/rag-pipeline-utils 2.4.4 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +345 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +4992 -1242
- package/dist/index.d.ts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
- package/dist/types/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +92 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +89 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +86 -0
- package/dist/types/connectors/memory-retriever.d.ts +91 -0
- package/dist/types/connectors/ollama-connector.d.ts +110 -0
- package/dist/types/connectors/openai-connector.d.ts +114 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +87 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +53 -29
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- package/src/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +101 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +96 -36
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +72 -217
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +41 -26
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/rag-service.js +322 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +617 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/connectors/anthropic-connector.js +93 -55
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +88 -32
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +43 -5
- package/src/connectors/memory-retriever.js +108 -15
- package/src/connectors/ollama-connector.js +83 -49
- package/src/connectors/openai-connector.js +108 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +209 -60
- package/src/core/plugin-registry.js +87 -11
- package/src/cost/cost-calculator.js +205 -32
- package/src/dev/dev-server.js +166 -11
- package/src/dev/hot-reload.js +40 -1
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +715 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/experimental.js +71 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +36 -1
- package/src/loader/document-reader.js +91 -0
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +212 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +134 -30
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- package/src/query.js +0 -117
|
@@ -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
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deprecation wrappers for public exports (see SEMVER.md, "Deprecation
|
|
5
|
+
* process"): the export keeps working and warns once per process when used.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const wrapped = new WeakMap();
|
|
9
|
+
const warned = new Set();
|
|
10
|
+
|
|
11
|
+
function warnOnce(code, message) {
|
|
12
|
+
if (warned.has(code)) return;
|
|
13
|
+
warned.add(code);
|
|
14
|
+
process.emitWarning(message, { type: 'DeprecationWarning', code });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a class so constructing it (or calling a static method) emits a
|
|
19
|
+
* one-time DeprecationWarning. A Proxy keeps the original's identity for
|
|
20
|
+
* prototype, name, statics, `extends` and `instanceof`, so instances created
|
|
21
|
+
* inside the library still satisfy `instanceof Wrapped`. The same class
|
|
22
|
+
* always gets the same wrapper, so aliases stay identical.
|
|
23
|
+
*
|
|
24
|
+
* @param {Function} Class - Class to wrap
|
|
25
|
+
* @param {object} info
|
|
26
|
+
* @param {string} info.name - Export name, used in the message and code
|
|
27
|
+
* @param {string} info.message - Full warning text
|
|
28
|
+
* @returns {Function} The wrapped class
|
|
29
|
+
*/
|
|
30
|
+
function deprecateClass(Class, { name, message }) {
|
|
31
|
+
if (wrapped.has(Class)) return wrapped.get(Class);
|
|
32
|
+
// SSOManager -> RAG_DEP_SSO_MANAGER, OAuth2Provider -> RAG_DEP_O_AUTH2_PROVIDER
|
|
33
|
+
const snake = name
|
|
34
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
35
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
36
|
+
.toUpperCase();
|
|
37
|
+
const code = `RAG_DEP_${snake}`;
|
|
38
|
+
const proxy = new Proxy(Class, {
|
|
39
|
+
construct(target, args, newTarget) {
|
|
40
|
+
warnOnce(code, message);
|
|
41
|
+
return Reflect.construct(
|
|
42
|
+
target,
|
|
43
|
+
args,
|
|
44
|
+
newTarget === proxy ? target : newTarget,
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
get(target, property, receiver) {
|
|
48
|
+
const value = Reflect.get(target, property, receiver);
|
|
49
|
+
// Only the class's own static methods; inherited Function.prototype
|
|
50
|
+
// members (call, bind, toString, Symbol.hasInstance for instanceof)
|
|
51
|
+
// must not warn.
|
|
52
|
+
const ownStatic =
|
|
53
|
+
typeof value === 'function' &&
|
|
54
|
+
property !== 'prototype' &&
|
|
55
|
+
Object.prototype.hasOwnProperty.call(target, property);
|
|
56
|
+
if (ownStatic) {
|
|
57
|
+
return function deprecatedStatic(...args) {
|
|
58
|
+
warnOnce(code, message);
|
|
59
|
+
return value.apply(this === proxy ? target : this, args);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
wrapped.set(Class, proxy);
|
|
66
|
+
return proxy;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { deprecateClass };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Embed a single query string with any supported embedder shape.
|
|
5
|
+
*
|
|
6
|
+
* Prefers embedQuery(); falls back to embed(). Accepts the common return
|
|
7
|
+
* shapes: a vector, a list of vectors, or [{ values | vector | embedding }].
|
|
8
|
+
*
|
|
9
|
+
* @param {object} embedder - Object with embedQuery() or embed()
|
|
10
|
+
* @param {string} query - Text to embed
|
|
11
|
+
* @param {{ signal?: AbortSignal }} [options] - Passed to the embedder as its
|
|
12
|
+
* second argument (only when given, so embedders keep their usual arity)
|
|
13
|
+
* @returns {Promise<number[]|undefined>} The query vector
|
|
14
|
+
*/
|
|
15
|
+
async function embedQuery(embedder, query, options) {
|
|
16
|
+
const args = options ? [query, options] : [query];
|
|
17
|
+
const raw = embedder.embedQuery
|
|
18
|
+
? await embedder.embedQuery(...args)
|
|
19
|
+
: await embedder.embed(...args);
|
|
20
|
+
const first = Array.isArray(raw) && typeof raw[0] !== 'number' ? raw[0] : raw;
|
|
21
|
+
if (Array.isArray(first)) return first;
|
|
22
|
+
return first?.values || first?.vector || first?.embedding;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} [embedder]
|
|
27
|
+
* @returns {boolean} Whether embedQuery() can use this embedder
|
|
28
|
+
*/
|
|
29
|
+
function canEmbed(embedder) {
|
|
30
|
+
return Boolean(embedder && (embedder.embedQuery || embedder.embed));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { embedQuery, canEmbed };
|
|
@@ -65,6 +65,21 @@ const ValidationPatterns = {
|
|
|
65
65
|
hex: /^[0-9a-fA-F]+$/,
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Test a shared pattern from start of string. The DangerousPatterns regexes
|
|
70
|
+
* carry the /g flag (needed by replace()), and RegExp#test() on a /g regex
|
|
71
|
+
* resumes from lastIndex left by the previous call, which made a match
|
|
72
|
+
* against one input skip part of the next and let payloads through.
|
|
73
|
+
*
|
|
74
|
+
* @param {RegExp} pattern
|
|
75
|
+
* @param {string} str
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function matchesPattern(pattern, str) {
|
|
79
|
+
pattern.lastIndex = 0;
|
|
80
|
+
return pattern.test(str);
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
/**
|
|
69
84
|
* Dangerous patterns that indicate attacks (compiled for performance)
|
|
70
85
|
*/
|
|
@@ -476,7 +491,7 @@ class InputSanitizer {
|
|
|
476
491
|
|
|
477
492
|
// Apply all XSS patterns
|
|
478
493
|
for (const pattern of DangerousPatterns.xss) {
|
|
479
|
-
if (pattern
|
|
494
|
+
if (matchesPattern(pattern, sanitized)) {
|
|
480
495
|
this.stats.blocked++;
|
|
481
496
|
sanitized = sanitized.replace(pattern, '');
|
|
482
497
|
}
|
|
@@ -497,7 +512,7 @@ class InputSanitizer {
|
|
|
497
512
|
|
|
498
513
|
// Check for SQL injection patterns
|
|
499
514
|
for (const pattern of DangerousPatterns.sql) {
|
|
500
|
-
if (pattern
|
|
515
|
+
if (matchesPattern(pattern, sanitized)) {
|
|
501
516
|
this.stats.blocked++;
|
|
502
517
|
if (this.options.throwOnInvalid) {
|
|
503
518
|
throw new Error('Potential SQL injection detected');
|
|
@@ -520,7 +535,7 @@ class InputSanitizer {
|
|
|
520
535
|
_sanitizeCommand(str) {
|
|
521
536
|
// Check for command injection patterns
|
|
522
537
|
for (const pattern of DangerousPatterns.command) {
|
|
523
|
-
if (pattern
|
|
538
|
+
if (matchesPattern(pattern, str)) {
|
|
524
539
|
this.stats.blocked++;
|
|
525
540
|
if (this.options.throwOnInvalid) {
|
|
526
541
|
throw new Error('Potential command injection detected');
|
|
@@ -560,7 +575,7 @@ class InputSanitizer {
|
|
|
560
575
|
|
|
561
576
|
// Check for path traversal patterns in both original and fully decoded versions
|
|
562
577
|
for (const pattern of DangerousPatterns.pathTraversal) {
|
|
563
|
-
if (pattern
|
|
578
|
+
if (matchesPattern(pattern, str) || matchesPattern(pattern, current)) {
|
|
564
579
|
this.stats.blocked++;
|
|
565
580
|
// SECURITY: Always throw on path traversal - it should never be silently sanitized
|
|
566
581
|
// Empty paths could resolve to application root, creating security vulnerabilities
|
package/src/utils/logger.js
CHANGED
|
@@ -31,9 +31,20 @@ const getLogLevel = () => {
|
|
|
31
31
|
// Determine if pretty printing should be enabled
|
|
32
32
|
const shouldUsePrettyPrint = () => {
|
|
33
33
|
// Enable pretty print in development or when explicitly requested
|
|
34
|
-
|
|
35
|
-
process.env.NODE_ENV === 'development' || process.env.LOG_PRETTY === 'true'
|
|
36
|
-
);
|
|
34
|
+
const requested =
|
|
35
|
+
process.env.NODE_ENV === 'development' || process.env.LOG_PRETTY === 'true';
|
|
36
|
+
return requested && isPinoPrettyAvailable();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// pino-pretty is an optional peer dependency; pino throws at startup if the
|
|
40
|
+
// transport target cannot be resolved, so fall back to JSON output instead.
|
|
41
|
+
const isPinoPrettyAvailable = () => {
|
|
42
|
+
try {
|
|
43
|
+
require.resolve('pino-pretty');
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
/**
|
|
@@ -68,6 +79,25 @@ const baseLogger = pino({
|
|
|
68
79
|
}),
|
|
69
80
|
});
|
|
70
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Coerce the meta argument into an object. Callers often pass
|
|
84
|
+
* console-style extras such as `logger.error("Failed:", error.message)`;
|
|
85
|
+
* spreading a string would log one key per character.
|
|
86
|
+
*
|
|
87
|
+
* @param {*} meta
|
|
88
|
+
* @returns {object}
|
|
89
|
+
*/
|
|
90
|
+
function normalizeMeta(meta) {
|
|
91
|
+
if (meta == null) return {};
|
|
92
|
+
if (meta instanceof Error) {
|
|
93
|
+
return { error: meta.message, stack: meta.stack };
|
|
94
|
+
}
|
|
95
|
+
if (typeof meta !== 'object' || Array.isArray(meta)) {
|
|
96
|
+
return { detail: meta };
|
|
97
|
+
}
|
|
98
|
+
return meta;
|
|
99
|
+
}
|
|
100
|
+
|
|
71
101
|
/**
|
|
72
102
|
* Enhanced logger with correlation ID support and structured context
|
|
73
103
|
*/
|
|
@@ -118,7 +148,7 @@ class StructuredLogger {
|
|
|
118
148
|
...this.context,
|
|
119
149
|
correlationId: this.correlationId || crypto.randomUUID(),
|
|
120
150
|
timestamp: new Date().toISOString(),
|
|
121
|
-
...meta,
|
|
151
|
+
...normalizeMeta(meta),
|
|
122
152
|
};
|
|
123
153
|
|
|
124
154
|
this.logger[level](logData, message);
|