@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.1

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 (40) hide show
  1. package/README.md +260 -19
  2. package/contracts/contract-schema.json +189 -0
  3. package/dist/index.cjs +19 -0
  4. package/dist/index.mjs +15 -1
  5. package/package.json +3 -2
  6. package/src/cli/commands/wizard.js +627 -0
  7. package/src/cli/input-sanitization-middleware.js +356 -0
  8. package/src/cli/templates/development.template.json +64 -0
  9. package/src/cli/templates/minimal.template.json +33 -0
  10. package/src/cli/templates/production.template.json +104 -0
  11. package/src/cli/templates/testing.template.json +45 -0
  12. package/src/config/connection-pool-config.json +259 -0
  13. package/src/config/memory-optimization.js +414 -0
  14. package/src/config/redaction-patterns.json +150 -0
  15. package/src/config/sanitization-rules.json +323 -0
  16. package/src/core/plugin-registry.js +266 -20
  17. package/src/dag/core/error-context.js +391 -0
  18. package/src/dag/core/execution-scheduler.js +350 -0
  19. package/src/dag/core/topology-validation.js +415 -0
  20. package/src/dag/dag-engine.js +26 -424
  21. package/src/dev/README.md +126 -0
  22. package/src/dev/dev-server.js +356 -0
  23. package/src/dev/examples/hot-reload-usage.js +355 -0
  24. package/src/dev/hot-reload.js +807 -0
  25. package/src/enterprise/sso-integration.js +67 -25
  26. package/src/index.js +19 -0
  27. package/src/observability/event-logger.js +62 -2
  28. package/src/plugins/optimized-openai-embedder.js +169 -0
  29. package/src/security/jwt-config.js +431 -0
  30. package/src/security/jwt-validator.js +701 -0
  31. package/src/utils/api-client-pool-integration.js +436 -0
  32. package/src/utils/batch-processor.js +710 -0
  33. package/src/utils/connection-pool.js +605 -0
  34. package/src/utils/contract-validator.js +460 -0
  35. package/src/utils/error-formatter.js +603 -0
  36. package/src/utils/examples/error-formatter-usage.js +351 -0
  37. package/src/utils/input-sanitizer.js +967 -0
  38. package/src/utils/memory-monitor.js +800 -0
  39. package/src/utils/secure-logger-config.js +352 -0
  40. package/src/utils/secure-logger.js +576 -0
@@ -0,0 +1,701 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Secure JWT Validator
5
+ *
6
+ * Provides hardened JWT validation with protection against common attacks:
7
+ * - Algorithm confusion attacks (none algorithm, algorithm switching)
8
+ * - Token substitution attacks
9
+ * - Weak signing algorithms
10
+ * - Missing or invalid claims
11
+ * - Replay attacks (with JTI tracking)
12
+ * - Token expiration and timing issues
13
+ *
14
+ * @module security/jwt-validator
15
+ * @since 2.3.0
16
+ */
17
+
18
+ const jwt = require('jsonwebtoken');
19
+ const crypto = require('crypto');
20
+ const { EventEmitter } = require('events');
21
+
22
+ /**
23
+ * Secure JWT signing and validation algorithms
24
+ * Explicitly disallow 'none' algorithm and weak algorithms
25
+ */
26
+ const SECURE_ALGORITHMS = {
27
+ // HMAC-based (symmetric)
28
+ HS256: 'HS256',
29
+ HS384: 'HS384',
30
+ HS512: 'HS512',
31
+
32
+ // RSA-based (asymmetric) - preferred for production
33
+ RS256: 'RS256',
34
+ RS384: 'RS384',
35
+ RS512: 'RS512',
36
+
37
+ // ECDSA-based (asymmetric) - preferred for performance
38
+ ES256: 'ES256',
39
+ ES384: 'ES384',
40
+ ES512: 'ES512',
41
+
42
+ // PSS (asymmetric)
43
+ PS256: 'PS256',
44
+ PS384: 'PS384',
45
+ PS512: 'PS512',
46
+ };
47
+
48
+ /**
49
+ * Minimum key sizes for secure JWT signing
50
+ */
51
+ const MIN_KEY_SIZES = {
52
+ HS256: 256, // bits
53
+ HS384: 384,
54
+ HS512: 512,
55
+ RS256: 2048,
56
+ RS384: 2048,
57
+ RS512: 2048,
58
+ ES256: 256,
59
+ ES384: 384,
60
+ ES512: 521,
61
+ PS256: 2048,
62
+ PS384: 2048,
63
+ PS512: 2048,
64
+ };
65
+
66
+ /**
67
+ * Required JWT claims for validation
68
+ */
69
+ const REQUIRED_CLAIMS = ['iat', 'exp', 'iss', 'aud'];
70
+
71
+ /**
72
+ * JWT Validator with hardened security
73
+ */
74
+ class JWTValidator extends EventEmitter {
75
+ constructor(options = {}) {
76
+ super();
77
+
78
+ this.config = {
79
+ // Algorithm configuration
80
+ algorithm: options.algorithm || 'HS256',
81
+ allowedAlgorithms: options.allowedAlgorithms || ['HS256'],
82
+
83
+ // Secret/Key configuration
84
+ secret: options.secret,
85
+ publicKey: options.publicKey,
86
+ privateKey: options.privateKey,
87
+
88
+ // Issuer and audience validation
89
+ issuer: options.issuer,
90
+ audience: options.audience,
91
+
92
+ // Timing configuration
93
+ clockTolerance: options.clockTolerance || 30, // seconds
94
+ maxTokenAge: options.maxTokenAge || 3600, // 1 hour in seconds
95
+ notBeforeTolerance: options.notBeforeTolerance || 30, // seconds
96
+
97
+ // Security options
98
+ requireJti: options.requireJti !== false, // Require JWT ID by default
99
+ requireSub: options.requireSub !== false, // Require subject by default
100
+ allowNoneAlgorithm: false, // Never allow 'none' algorithm
101
+ strictValidation: options.strictValidation !== false, // Strict by default
102
+
103
+ // JTI tracking for replay protection
104
+ enableJtiTracking: options.enableJtiTracking !== false,
105
+ jtiExpirationMs: options.jtiExpirationMs || 7 * 24 * 60 * 60 * 1000, // 7 days
106
+
107
+ // Additional validation
108
+ validateClaims: options.validateClaims || {},
109
+
110
+ // Audit logging
111
+ enableAuditLog: options.enableAuditLog !== false,
112
+ };
113
+
114
+ // Validate configuration
115
+ this._validateConfig();
116
+
117
+ // Initialize JTI tracking
118
+ if (this.config.enableJtiTracking) {
119
+ this.usedJtis = new Map(); // jti -> expiration timestamp (for external tokens)
120
+ this.selfSignedJtis = new Set(); // JTIs of tokens we signed ourselves
121
+ this._startJtiCleanup();
122
+ }
123
+
124
+ // Statistics
125
+ this.stats = {
126
+ tokensGenerated: 0,
127
+ tokensValidated: 0,
128
+ validationFailures: 0,
129
+ algorithmMismatch: 0,
130
+ expiredTokens: 0,
131
+ invalidSignature: 0,
132
+ replayAttempts: 0,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Validate configuration on initialization
138
+ * @private
139
+ */
140
+ _validateConfig() {
141
+ const {
142
+ algorithm,
143
+ allowedAlgorithms,
144
+ secret,
145
+ publicKey,
146
+ privateKey,
147
+ issuer,
148
+ audience,
149
+ strictValidation,
150
+ } = this.config;
151
+
152
+ // Validate required claims configuration
153
+ if (strictValidation !== false) {
154
+ if (!issuer) {
155
+ throw new Error('issuer is required');
156
+ }
157
+ if (!audience) {
158
+ throw new Error('audience is required');
159
+ }
160
+ }
161
+
162
+ // Validate algorithm
163
+ if (!SECURE_ALGORITHMS[algorithm]) {
164
+ throw new Error(
165
+ `Unsupported algorithm: ${algorithm}. Use one of: ${Object.keys(SECURE_ALGORITHMS).join(', ')}`,
166
+ );
167
+ }
168
+
169
+ // Validate allowed algorithms
170
+ if (!Array.isArray(allowedAlgorithms) || allowedAlgorithms.length === 0) {
171
+ throw new Error('allowedAlgorithms must be a non-empty array');
172
+ }
173
+
174
+ for (const alg of allowedAlgorithms) {
175
+ if (!SECURE_ALGORITHMS[alg]) {
176
+ throw new Error(`Invalid algorithm in allowedAlgorithms: ${alg}`);
177
+ }
178
+ if (alg === 'none') {
179
+ throw new Error('Algorithm "none" is not allowed for security reasons');
180
+ }
181
+ }
182
+
183
+ // Validate secret/key configuration
184
+ const isSymmetric = algorithm.startsWith('HS');
185
+ const isAsymmetric =
186
+ algorithm.startsWith('RS') ||
187
+ algorithm.startsWith('ES') ||
188
+ algorithm.startsWith('PS');
189
+
190
+ if (isSymmetric && !secret) {
191
+ throw new Error(
192
+ 'secret is required for HMAC algorithms (HS256, HS384, HS512)',
193
+ );
194
+ }
195
+
196
+ if (isAsymmetric) {
197
+ if (!publicKey) {
198
+ throw new Error('publicKey is required for asymmetric algorithms');
199
+ }
200
+ if (!privateKey) {
201
+ console.warn(
202
+ '[JWT_VALIDATOR] Warning: privateKey not provided. Token signing will not be available.',
203
+ );
204
+ }
205
+ }
206
+
207
+ // Validate secret strength for HMAC
208
+ if (isSymmetric && secret) {
209
+ const minBits = MIN_KEY_SIZES[algorithm];
210
+ const secretBits = Buffer.byteLength(secret, 'utf8') * 8;
211
+
212
+ if (secretBits < minBits) {
213
+ throw new Error(
214
+ `Secret is too weak for ${algorithm}. Minimum ${minBits} bits required, got ${secretBits} bits. ` +
215
+ `Use a secret of at least ${Math.ceil(minBits / 8)} bytes.`,
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Generate a secure JWT token
223
+ *
224
+ * @param {Object} payload - Token payload
225
+ * @param {Object} options - Additional signing options
226
+ * @returns {string} Signed JWT token
227
+ *
228
+ * @example
229
+ * const token = validator.sign({
230
+ * sub: 'user-123',
231
+ * roles: ['admin'],
232
+ * tenant: 'acme-corp'
233
+ * });
234
+ */
235
+ sign(payload, options = {}) {
236
+ try {
237
+ // Validate payload
238
+ if (!payload || typeof payload !== 'object') {
239
+ throw new Error('Payload must be an object');
240
+ }
241
+
242
+ // Prevent algorithm override in payload
243
+ if (payload.alg) {
244
+ throw new Error(
245
+ 'Algorithm cannot be specified in payload (security risk)',
246
+ );
247
+ }
248
+
249
+ // Generate JTI if required
250
+ const jti = this.config.requireJti
251
+ ? options.jti || crypto.randomUUID()
252
+ : options.jti;
253
+
254
+ // Build complete payload with required claims
255
+ const now = Math.floor(Date.now() / 1000);
256
+ const expiresIn = options.expiresIn || this.config.maxTokenAge;
257
+
258
+ const completePayload = {
259
+ ...payload,
260
+ iss: this.config.issuer,
261
+ aud: this.config.audience,
262
+ iat: now,
263
+ exp: now + expiresIn,
264
+ nbf: options.notBefore || now,
265
+ };
266
+
267
+ // Add JTI if required
268
+ if (jti) {
269
+ completePayload.jti = jti;
270
+ }
271
+
272
+ // Add subject if required
273
+ if (this.config.requireSub && !completePayload.sub) {
274
+ throw new Error('Subject (sub) claim is required but not provided');
275
+ }
276
+
277
+ // Determine signing key
278
+ const signingKey = this.config.algorithm.startsWith('HS')
279
+ ? this.config.secret
280
+ : this.config.privateKey;
281
+
282
+ if (!signingKey) {
283
+ throw new Error(
284
+ 'No signing key available. Ensure secret or privateKey is configured.',
285
+ );
286
+ }
287
+
288
+ // Sign token with explicit algorithm
289
+ const token = jwt.sign(completePayload, signingKey, {
290
+ algorithm: this.config.algorithm,
291
+ // Do not allow algorithm to be overridden
292
+ header: {
293
+ alg: this.config.algorithm,
294
+ typ: 'JWT',
295
+ },
296
+ });
297
+
298
+ // Track self-signed JTIs with special handling to allow multiple verifications
299
+ // Self-signed tokens are tracked to prevent JTI collisions with external tokens
300
+ // but they can be verified multiple times (marked as reusable)
301
+ if (this.config.enableJtiTracking && jti) {
302
+ this.selfSignedJtis.add(jti);
303
+ // Also track in usedJtis with reusable flag to prevent external tokens from using same JTI
304
+ const expirationTime = (now + expiresIn) * 1000;
305
+ this.usedJtis.set(jti, { expirationTime, reusable: true });
306
+ }
307
+
308
+ // Update statistics
309
+ this.stats.tokensGenerated++;
310
+
311
+ // Emit audit event
312
+ if (this.config.enableAuditLog) {
313
+ this.emit('token_generated', {
314
+ jti,
315
+ sub: completePayload.sub,
316
+ iss: completePayload.iss,
317
+ exp: completePayload.exp,
318
+ timestamp: new Date().toISOString(),
319
+ });
320
+ }
321
+
322
+ return token;
323
+ } catch (error) {
324
+ this.emit('signing_error', {
325
+ error: error.message,
326
+ timestamp: new Date().toISOString(),
327
+ });
328
+ throw new Error(`JWT signing failed: ${error.message}`);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Verify and decode a JWT token with hardened validation
334
+ *
335
+ * @param {string} token - JWT token to verify
336
+ * @param {Object} options - Verification options
337
+ * @returns {Object} Decoded and validated payload
338
+ *
339
+ * @example
340
+ * try {
341
+ * const payload = validator.verify(token);
342
+ * console.log('Valid token for user:', payload.sub);
343
+ * } catch (error) {
344
+ * console.error('Invalid token:', error.message);
345
+ * }
346
+ */
347
+ verify(token, options = {}) {
348
+ try {
349
+ // Basic validation
350
+ if (!token || typeof token !== 'string') {
351
+ throw new Error('Token must be a non-empty string');
352
+ }
353
+
354
+ // Prevent header manipulation - decode header first to check algorithm
355
+ const header = this._decodeTokenHeader(token);
356
+
357
+ // CRITICAL: Explicitly reject 'none' algorithm FIRST (before other checks)
358
+ if (header.alg === 'none' || header.alg.toLowerCase() === 'none') {
359
+ this.stats.validationFailures++;
360
+ this.emit('none_algorithm_rejected', {
361
+ timestamp: new Date().toISOString(),
362
+ });
363
+ throw new Error('Algorithm "none" is not allowed');
364
+ }
365
+
366
+ // CRITICAL: Verify algorithm matches expected algorithm
367
+ if (!this.config.allowedAlgorithms.includes(header.alg)) {
368
+ this.stats.algorithmMismatch++;
369
+ this.emit('algorithm_mismatch', {
370
+ expected: this.config.allowedAlgorithms,
371
+ received: header.alg,
372
+ timestamp: new Date().toISOString(),
373
+ });
374
+ throw new Error(
375
+ `Algorithm mismatch: expected one of [${this.config.allowedAlgorithms.join(', ')}], got ${header.alg}`,
376
+ );
377
+ }
378
+
379
+ // Determine verification key
380
+ const verificationKey = header.alg.startsWith('HS')
381
+ ? this.config.secret
382
+ : this.config.publicKey;
383
+
384
+ if (!verificationKey) {
385
+ throw new Error('No verification key available');
386
+ }
387
+
388
+ // Verify token with strict options
389
+ const verifyOptions = {
390
+ algorithms: this.config.allowedAlgorithms,
391
+ clockTolerance: this.config.clockTolerance,
392
+ // Complete validation checks
393
+ complete: false, // We want payload only after validation
394
+ };
395
+
396
+ // Only validate issuer/audience if strictValidation is enabled
397
+ // This prevents duplicate validation (jsonwebtoken validates these if provided)
398
+ if (this.config.strictValidation) {
399
+ verifyOptions.issuer = this.config.issuer;
400
+ verifyOptions.audience = this.config.audience;
401
+ }
402
+
403
+ // Only add maxAge if explicitly requested (optional validation)
404
+ if (options.maxAge || this.config.maxTokenAge < Infinity) {
405
+ verifyOptions.maxAge = options.maxAge || `${this.config.maxTokenAge}s`;
406
+ }
407
+
408
+ const payload = jwt.verify(token, verificationKey, verifyOptions);
409
+
410
+ // Additional claim validation
411
+ this._validateRequiredClaims(payload);
412
+
413
+ // Check for replay attacks using JTI
414
+ // NOTE: This uses a check-then-set pattern which is safe for this use case:
415
+ // - Self-signed tokens (reusable) can be verified multiple times by design
416
+ // - External tokens (non-reusable): if race condition occurs, both requests succeed on first use
417
+ // (acceptable) but subsequent replays are blocked (secure)
418
+ if (this.config.enableJtiTracking && payload.jti) {
419
+ // Check if this is a self-signed (reusable) token
420
+ const isSelfSigned = this.selfSignedJtis.has(payload.jti);
421
+
422
+ if (!this.usedJtis.has(payload.jti)) {
423
+ // First time seeing this JTI - atomically track it
424
+ const expirationTime = payload.exp * 1000;
425
+ this.usedJtis.set(payload.jti, {
426
+ expirationTime,
427
+ reusable: isSelfSigned,
428
+ });
429
+ } else {
430
+ // JTI has been seen before
431
+ const jtiEntry = this.usedJtis.get(payload.jti);
432
+
433
+ // Self-signed tokens are reusable - allow multiple verifications
434
+ if (jtiEntry.reusable && isSelfSigned) {
435
+ // This is valid for refresh flows and load balancer retries
436
+ } else {
437
+ // External token being replayed - this is a security violation
438
+ this.stats.replayAttempts++;
439
+ this.emit('replay_detected', {
440
+ jti: payload.jti,
441
+ sub: payload.sub,
442
+ isSelfSigned,
443
+ timestamp: new Date().toISOString(),
444
+ });
445
+ throw new Error('Token replay detected: JTI has already been used');
446
+ }
447
+ }
448
+ }
449
+
450
+ // Validate custom claims if specified
451
+ if (
452
+ this.config.validateClaims &&
453
+ Object.keys(this.config.validateClaims).length > 0
454
+ ) {
455
+ this._validateCustomClaims(payload, this.config.validateClaims);
456
+ }
457
+
458
+ // Update statistics
459
+ this.stats.tokensValidated++;
460
+
461
+ // Emit audit event
462
+ if (this.config.enableAuditLog) {
463
+ this.emit('token_validated', {
464
+ jti: payload.jti,
465
+ sub: payload.sub,
466
+ iss: payload.iss,
467
+ exp: payload.exp,
468
+ timestamp: new Date().toISOString(),
469
+ });
470
+ }
471
+
472
+ return payload;
473
+ } catch (error) {
474
+ // Update statistics based on error type
475
+ if (error.name === 'TokenExpiredError') {
476
+ this.stats.expiredTokens++;
477
+ } else if (error.name === 'JsonWebTokenError') {
478
+ this.stats.invalidSignature++;
479
+ } else {
480
+ this.stats.validationFailures++;
481
+ }
482
+
483
+ // Emit audit event for validation failure
484
+ if (this.config.enableAuditLog) {
485
+ this.emit('validation_failed', {
486
+ error: error.message,
487
+ errorType: error.name,
488
+ timestamp: new Date().toISOString(),
489
+ });
490
+ }
491
+
492
+ // Re-throw with context
493
+ if (error.name === 'TokenExpiredError') {
494
+ throw new Error('JWT token has expired');
495
+ } else if (error.name === 'JsonWebTokenError') {
496
+ throw new Error('Invalid JWT token signature or structure');
497
+ } else if (error.name === 'NotBeforeError') {
498
+ throw new Error('JWT token not yet valid (nbf claim)');
499
+ }
500
+
501
+ throw new Error(`JWT verification failed: ${error.message}`);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Decode token header without verification (for algorithm checking)
507
+ * @private
508
+ */
509
+ _decodeTokenHeader(token) {
510
+ try {
511
+ const parts = token.split('.');
512
+ if (parts.length !== 3) {
513
+ throw new Error('Invalid token structure');
514
+ }
515
+
516
+ const header = JSON.parse(
517
+ Buffer.from(parts[0], 'base64url').toString('utf8'),
518
+ );
519
+ return header;
520
+ } catch (error) {
521
+ throw new Error(`Failed to decode token header: ${error.message}`);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Validate required claims are present
527
+ * @private
528
+ */
529
+ _validateRequiredClaims(payload) {
530
+ const missingClaims = [];
531
+
532
+ // Always require iat and exp (timing claims)
533
+ for (const claim of ['iat', 'exp']) {
534
+ if (!payload[claim]) {
535
+ missingClaims.push(claim);
536
+ }
537
+ }
538
+
539
+ // Note: iss/aud validation is handled by jwt.verify() when strictValidation is enabled
540
+ // (see verifyOptions configuration in verify() method)
541
+ // No need to duplicate that validation here
542
+
543
+ if (this.config.requireJti && !payload.jti) {
544
+ missingClaims.push('jti');
545
+ }
546
+
547
+ if (this.config.requireSub && !payload.sub) {
548
+ missingClaims.push('sub');
549
+ }
550
+
551
+ if (missingClaims.length > 0) {
552
+ throw new Error(`Missing required claims: ${missingClaims.join(', ')}`);
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Validate custom claims
558
+ * @private
559
+ */
560
+ _validateCustomClaims(payload, expectedClaims) {
561
+ for (const [claim, expectedValue] of Object.entries(expectedClaims)) {
562
+ if (typeof expectedValue === 'function') {
563
+ // Custom validation function
564
+ if (!expectedValue(payload[claim], payload)) {
565
+ throw new Error(`Custom claim validation failed for: ${claim}`);
566
+ }
567
+ } else if (Array.isArray(expectedValue)) {
568
+ // Value must be in array
569
+ if (!expectedValue.includes(payload[claim])) {
570
+ throw new Error(
571
+ `Claim ${claim} must be one of: ${expectedValue.join(', ')}`,
572
+ );
573
+ }
574
+ } else {
575
+ // Exact match
576
+ if (payload[claim] !== expectedValue) {
577
+ throw new Error(
578
+ `Claim ${claim} mismatch: expected ${expectedValue}, got ${payload[claim]}`,
579
+ );
580
+ }
581
+ }
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Start periodic cleanup of expired JTIs
587
+ * @private
588
+ */
589
+ _startJtiCleanup() {
590
+ this.jtiCleanupInterval = setInterval(
591
+ () => {
592
+ const now = Date.now();
593
+ let cleanedCount = 0;
594
+
595
+ for (const [jti, jtiEntry] of this.usedJtis.entries()) {
596
+ // Handle both old format (number) and new format (object)
597
+ const expirationTime =
598
+ typeof jtiEntry === 'number' ? jtiEntry : jtiEntry.expirationTime;
599
+ if (expirationTime < now) {
600
+ this.usedJtis.delete(jti);
601
+ // Also remove from selfSignedJtis if it's there
602
+ this.selfSignedJtis.delete(jti);
603
+ cleanedCount++;
604
+ }
605
+ }
606
+
607
+ if (cleanedCount > 0 && this.config.enableAuditLog) {
608
+ this.emit('jti_cleanup', {
609
+ cleanedCount,
610
+ remainingCount: this.usedJtis.size,
611
+ timestamp: new Date().toISOString(),
612
+ });
613
+ }
614
+ },
615
+ 60 * 60 * 1000,
616
+ ); // Run every hour
617
+
618
+ // Don't block process exit
619
+ if (this.jtiCleanupInterval.unref) {
620
+ this.jtiCleanupInterval.unref();
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Decode token without verification (use with caution)
626
+ *
627
+ * @param {string} token - JWT token to decode
628
+ * @returns {Object} Decoded payload (unverified)
629
+ */
630
+ decode(token) {
631
+ try {
632
+ return jwt.decode(token, { complete: false });
633
+ } catch (error) {
634
+ throw new Error(`Failed to decode token: ${error.message}`);
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Get validator statistics
640
+ *
641
+ * @returns {Object} Statistics object
642
+ */
643
+ getStats() {
644
+ return {
645
+ ...this.stats,
646
+ trackedJtis: this.config.enableJtiTracking ? this.usedJtis.size : 0,
647
+ };
648
+ }
649
+
650
+ /**
651
+ * Reset statistics
652
+ */
653
+ resetStats() {
654
+ this.stats = {
655
+ tokensGenerated: 0,
656
+ tokensValidated: 0,
657
+ validationFailures: 0,
658
+ algorithmMismatch: 0,
659
+ expiredTokens: 0,
660
+ invalidSignature: 0,
661
+ replayAttempts: 0,
662
+ };
663
+ }
664
+
665
+ /**
666
+ * Clear JTI tracking
667
+ */
668
+ clearJtiTracking() {
669
+ if (this.config.enableJtiTracking) {
670
+ this.usedJtis.clear();
671
+ this.selfSignedJtis.clear();
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Destroy validator and cleanup resources
677
+ */
678
+ destroy() {
679
+ if (this.jtiCleanupInterval) {
680
+ clearInterval(this.jtiCleanupInterval);
681
+ this.jtiCleanupInterval = null;
682
+ }
683
+
684
+ if (this.usedJtis) {
685
+ this.usedJtis.clear();
686
+ }
687
+
688
+ if (this.selfSignedJtis) {
689
+ this.selfSignedJtis.clear();
690
+ }
691
+
692
+ this.removeAllListeners();
693
+ }
694
+ }
695
+
696
+ module.exports = {
697
+ JWTValidator,
698
+ SECURE_ALGORITHMS,
699
+ MIN_KEY_SIZES,
700
+ REQUIRED_CLAIMS,
701
+ };