agentshield-sdk 7.0.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 (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,603 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Allowlist, Confidence Calibration, Feedback Loop & Scan Cache
5
+ *
6
+ * Features:
7
+ * - Allowlist/bypass rules for false positive management
8
+ * - Confidence calibration based on traffic patterns
9
+ * - Feedback loop API for continuous improvement
10
+ * - LRU scan result cache for performance
11
+ */
12
+
13
+ // =========================================================================
14
+ // Allowlist / Bypass Rules
15
+ // =========================================================================
16
+
17
+ class Allowlist {
18
+ constructor(options = {}) {
19
+ this.rules = [];
20
+ this.globalPatterns = [];
21
+ this.perCategoryBypasses = {};
22
+ this.stats = { checked: 0, bypassed: 0 };
23
+
24
+ if (options.rules) {
25
+ for (const rule of options.rules) {
26
+ this.addRule(rule);
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Add an allowlist rule.
33
+ * @param {Object} rule - { pattern: string|RegExp, category?: string, reason: string, addedBy?: string }
34
+ */
35
+ addRule(rule) {
36
+ const compiled = {
37
+ id: `allow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
38
+ pattern: typeof rule.pattern === 'string' ? new RegExp(rule.pattern, 'i') : rule.pattern,
39
+ patternSource: typeof rule.pattern === 'string' ? rule.pattern : rule.pattern.source,
40
+ category: rule.category || null,
41
+ reason: rule.reason || 'No reason provided',
42
+ addedBy: rule.addedBy || 'system',
43
+ addedAt: new Date().toISOString(),
44
+ hitCount: 0
45
+ };
46
+
47
+ this.rules.push(compiled);
48
+
49
+ if (compiled.category) {
50
+ if (!this.perCategoryBypasses[compiled.category]) {
51
+ this.perCategoryBypasses[compiled.category] = [];
52
+ }
53
+ this.perCategoryBypasses[compiled.category].push(compiled);
54
+ } else {
55
+ this.globalPatterns.push(compiled);
56
+ }
57
+
58
+ return compiled.id;
59
+ }
60
+
61
+ /**
62
+ * Check if an input should bypass scanning for a specific threat.
63
+ * @param {string} text - Input text
64
+ * @param {Object} threat - Threat object with { category, description }
65
+ * @returns {{ allowed: boolean, rule?: Object }}
66
+ */
67
+ check(text, threat = {}) {
68
+ this.stats.checked++;
69
+
70
+ // Check category-specific bypasses first
71
+ if (threat.category && this.perCategoryBypasses[threat.category]) {
72
+ for (const rule of this.perCategoryBypasses[threat.category]) {
73
+ if (rule.pattern.test(text)) {
74
+ rule.hitCount++;
75
+ this.stats.bypassed++;
76
+ return { allowed: true, rule: { id: rule.id, reason: rule.reason } };
77
+ }
78
+ }
79
+ }
80
+
81
+ // Check global bypasses
82
+ for (const rule of this.globalPatterns) {
83
+ if (rule.pattern.test(text)) {
84
+ rule.hitCount++;
85
+ this.stats.bypassed++;
86
+ return { allowed: true, rule: { id: rule.id, reason: rule.reason } };
87
+ }
88
+ }
89
+
90
+ return { allowed: false };
91
+ }
92
+
93
+ /**
94
+ * Filter threats, removing any that match allowlist rules.
95
+ * @param {string} text - Input text
96
+ * @param {Array} threats - Array of threat objects
97
+ * @returns {{ filtered: Array, bypassed: Array }}
98
+ */
99
+ filterThreats(text, threats) {
100
+ const filtered = [];
101
+ const bypassed = [];
102
+
103
+ for (const threat of threats) {
104
+ const result = this.check(text, threat);
105
+ if (result.allowed) {
106
+ bypassed.push({ ...threat, bypassRule: result.rule });
107
+ } else {
108
+ filtered.push(threat);
109
+ }
110
+ }
111
+
112
+ return { filtered, bypassed };
113
+ }
114
+
115
+ /**
116
+ * Remove an allowlist rule by ID.
117
+ */
118
+ removeRule(ruleId) {
119
+ this.rules = this.rules.filter(r => r.id !== ruleId);
120
+ this.globalPatterns = this.globalPatterns.filter(r => r.id !== ruleId);
121
+ for (const cat of Object.keys(this.perCategoryBypasses)) {
122
+ this.perCategoryBypasses[cat] = this.perCategoryBypasses[cat].filter(r => r.id !== ruleId);
123
+ }
124
+ return true;
125
+ }
126
+
127
+ /**
128
+ * Get all rules with their hit counts.
129
+ */
130
+ getRules() {
131
+ return this.rules.map(r => ({
132
+ id: r.id,
133
+ pattern: r.patternSource,
134
+ category: r.category,
135
+ reason: r.reason,
136
+ addedBy: r.addedBy,
137
+ addedAt: r.addedAt,
138
+ hitCount: r.hitCount
139
+ }));
140
+ }
141
+
142
+ /**
143
+ * Get stats.
144
+ */
145
+ getStats() {
146
+ return {
147
+ ...this.stats,
148
+ ruleCount: this.rules.length,
149
+ bypassRate: this.stats.checked > 0
150
+ ? `${((this.stats.bypassed / this.stats.checked) * 100).toFixed(1)}%`
151
+ : '0%'
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Export rules as JSON for persistence.
157
+ */
158
+ exportRules() {
159
+ return JSON.stringify(this.getRules(), null, 2);
160
+ }
161
+
162
+ /**
163
+ * Import rules from JSON.
164
+ */
165
+ importRules(json) {
166
+ const rules = typeof json === 'string' ? JSON.parse(json) : json;
167
+ for (const rule of rules) {
168
+ this.addRule(rule);
169
+ }
170
+ return rules.length;
171
+ }
172
+ }
173
+
174
+ // =========================================================================
175
+ // Confidence Calibration
176
+ // =========================================================================
177
+
178
+ class ConfidenceCalibrator {
179
+ constructor(options = {}) {
180
+ this.windowSize = options.windowSize || 1000;
181
+ this.history = [];
182
+ this.falsePositives = 0;
183
+ this.truePositives = 0;
184
+ this.falseNegatives = 0;
185
+ this.trueNegatives = 0;
186
+ this.categoryStats = {};
187
+ this.thresholdSuggestions = null;
188
+ }
189
+
190
+ /**
191
+ * Record a scan result with ground truth feedback.
192
+ * @param {Object} scanResult - The scan result
193
+ * @param {boolean} wasActuallyMalicious - Ground truth
194
+ */
195
+ record(scanResult, wasActuallyMalicious) {
196
+ const detected = scanResult.threats && scanResult.threats.length > 0;
197
+
198
+ if (detected && wasActuallyMalicious) this.truePositives++;
199
+ else if (detected && !wasActuallyMalicious) this.falsePositives++;
200
+ else if (!detected && wasActuallyMalicious) this.falseNegatives++;
201
+ else this.trueNegatives++;
202
+
203
+ // Track per-category
204
+ if (detected) {
205
+ for (const threat of scanResult.threats) {
206
+ const cat = threat.category;
207
+ if (!this.categoryStats[cat]) {
208
+ this.categoryStats[cat] = { tp: 0, fp: 0 };
209
+ }
210
+ if (wasActuallyMalicious) this.categoryStats[cat].tp++;
211
+ else this.categoryStats[cat].fp++;
212
+ }
213
+ }
214
+
215
+ this.history.push({
216
+ detected,
217
+ actual: wasActuallyMalicious,
218
+ threats: (scanResult.threats || []).map(t => t.category),
219
+ timestamp: Date.now()
220
+ });
221
+
222
+ // Trim history
223
+ while (this.history.length > this.windowSize) {
224
+ this.history.shift();
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get current calibration metrics.
230
+ */
231
+ getMetrics() {
232
+ const total = this.truePositives + this.falsePositives + this.falseNegatives + this.trueNegatives;
233
+ if (total === 0) return { status: 'insufficient_data', total: 0 };
234
+
235
+ const precision = this.truePositives + this.falsePositives > 0
236
+ ? this.truePositives / (this.truePositives + this.falsePositives)
237
+ : 0;
238
+
239
+ const recall = this.truePositives + this.falseNegatives > 0
240
+ ? this.truePositives / (this.truePositives + this.falseNegatives)
241
+ : 0;
242
+
243
+ const f1 = precision + recall > 0
244
+ ? 2 * (precision * recall) / (precision + recall)
245
+ : 0;
246
+
247
+ const falsePositiveRate = this.falsePositives + this.trueNegatives > 0
248
+ ? this.falsePositives / (this.falsePositives + this.trueNegatives)
249
+ : 0;
250
+
251
+ return {
252
+ status: 'calibrated',
253
+ total,
254
+ truePositives: this.truePositives,
255
+ falsePositives: this.falsePositives,
256
+ trueNegatives: this.trueNegatives,
257
+ falseNegatives: this.falseNegatives,
258
+ precision: parseFloat((precision * 100).toFixed(1)),
259
+ recall: parseFloat((recall * 100).toFixed(1)),
260
+ f1Score: parseFloat((f1 * 100).toFixed(1)),
261
+ falsePositiveRate: parseFloat((falsePositiveRate * 100).toFixed(1)),
262
+ categoryBreakdown: this.getCategoryBreakdown()
263
+ };
264
+ }
265
+
266
+ getCategoryBreakdown() {
267
+ const result = {};
268
+ for (const [cat, stats] of Object.entries(this.categoryStats)) {
269
+ const total = stats.tp + stats.fp;
270
+ result[cat] = {
271
+ truePositives: stats.tp,
272
+ falsePositives: stats.fp,
273
+ precision: total > 0 ? parseFloat(((stats.tp / total) * 100).toFixed(1)) : 0
274
+ };
275
+ }
276
+ return result;
277
+ }
278
+
279
+ /**
280
+ * Suggest threshold adjustments based on collected data.
281
+ */
282
+ suggestThresholds() {
283
+ const metrics = this.getMetrics();
284
+ if (metrics.status === 'insufficient_data') {
285
+ return { status: 'insufficient_data', message: `Need at least 1 data point. Have ${metrics.total}.` };
286
+ }
287
+
288
+ const suggestions = [];
289
+
290
+ if (metrics.falsePositiveRate > 20) {
291
+ suggestions.push({
292
+ action: 'lower_sensitivity',
293
+ reason: `False positive rate is ${metrics.falsePositiveRate}% (target: <10%)`,
294
+ suggestion: 'Consider switching from "high" to "medium" sensitivity'
295
+ });
296
+ }
297
+
298
+ if (metrics.recall < 80) {
299
+ suggestions.push({
300
+ action: 'raise_sensitivity',
301
+ reason: `Recall is ${metrics.recall}% (target: >90%)`,
302
+ suggestion: 'Consider switching from current sensitivity to "high"'
303
+ });
304
+ }
305
+
306
+ // Per-category suggestions
307
+ for (const [cat, stats] of Object.entries(this.categoryStats)) {
308
+ const total = stats.tp + stats.fp;
309
+ if (total >= 5 && stats.fp / total > 0.5) {
310
+ suggestions.push({
311
+ action: 'add_allowlist',
312
+ category: cat,
313
+ reason: `Category "${cat}" has ${((stats.fp / total) * 100).toFixed(0)}% false positive rate`,
314
+ suggestion: `Consider adding allowlist rules for "${cat}" category`
315
+ });
316
+ }
317
+ }
318
+
319
+ if (suggestions.length === 0) {
320
+ suggestions.push({
321
+ action: 'none',
322
+ reason: 'Current thresholds look good',
323
+ suggestion: `Precision: ${metrics.precision}%, Recall: ${metrics.recall}%`
324
+ });
325
+ }
326
+
327
+ this.thresholdSuggestions = suggestions;
328
+ return { status: 'ok', metrics, suggestions };
329
+ }
330
+
331
+ /**
332
+ * Reset calibration data.
333
+ */
334
+ reset() {
335
+ this.history = [];
336
+ this.falsePositives = 0;
337
+ this.truePositives = 0;
338
+ this.falseNegatives = 0;
339
+ this.trueNegatives = 0;
340
+ this.categoryStats = {};
341
+ this.thresholdSuggestions = null;
342
+ }
343
+ }
344
+
345
+ // =========================================================================
346
+ // Feedback Loop API
347
+ // =========================================================================
348
+
349
+ class FeedbackLoop {
350
+ constructor(options = {}) {
351
+ this.calibrator = options.calibrator || new ConfidenceCalibrator();
352
+ this.allowlist = options.allowlist || new Allowlist();
353
+ this.pendingReviews = [];
354
+ this.maxPending = options.maxPending || 100;
355
+ this.onFeedback = options.onFeedback || null;
356
+ this.stats = { falsePositives: 0, missed: 0, confirmed: 0 };
357
+ }
358
+
359
+ /**
360
+ * Report a false positive.
361
+ * @param {string} text - The input that was incorrectly flagged
362
+ * @param {Object} scanResult - The original scan result
363
+ * @param {Object} metadata - Additional context
364
+ */
365
+ reportFalsePositive(text, scanResult, metadata = {}) {
366
+ this.stats.falsePositives++;
367
+ this.calibrator.record(scanResult, false);
368
+
369
+ const entry = {
370
+ id: `fp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
371
+ type: 'false_positive',
372
+ text: text.substring(0, 500),
373
+ threats: scanResult.threats || [],
374
+ metadata,
375
+ timestamp: new Date().toISOString(),
376
+ status: 'pending'
377
+ };
378
+
379
+ this.pendingReviews.push(entry);
380
+ while (this.pendingReviews.length > this.maxPending) {
381
+ this.pendingReviews.shift();
382
+ }
383
+
384
+ if (this.onFeedback) { try { this.onFeedback(entry); } catch (e) { console.error('[Agent Shield] onFeedback callback error:', e.message); } }
385
+ return entry.id;
386
+ }
387
+
388
+ /**
389
+ * Report a missed attack (false negative).
390
+ * @param {string} text - The input that should have been flagged
391
+ * @param {Object} scanResult - The original scan result (which missed the threat)
392
+ * @param {Object} metadata - Additional context
393
+ */
394
+ reportMissed(text, scanResult, metadata = {}) {
395
+ this.stats.missed++;
396
+ this.calibrator.record(scanResult, true);
397
+
398
+ const entry = {
399
+ id: `fn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
400
+ type: 'false_negative',
401
+ text: text.substring(0, 500),
402
+ threats: scanResult.threats || [],
403
+ metadata,
404
+ timestamp: new Date().toISOString(),
405
+ status: 'pending'
406
+ };
407
+
408
+ this.pendingReviews.push(entry);
409
+ while (this.pendingReviews.length > this.maxPending) {
410
+ this.pendingReviews.shift();
411
+ }
412
+
413
+ if (this.onFeedback) { try { this.onFeedback(entry); } catch (e) { console.error('[Agent Shield] onFeedback callback error:', e.message); } }
414
+ return entry.id;
415
+ }
416
+
417
+ /**
418
+ * Confirm a detection was correct (true positive).
419
+ */
420
+ confirmDetection(scanResult) {
421
+ this.stats.confirmed++;
422
+ this.calibrator.record(scanResult, true);
423
+ }
424
+
425
+ /**
426
+ * Confirm a pass was correct (true negative).
427
+ */
428
+ confirmSafe(scanResult) {
429
+ this.calibrator.record(scanResult, false);
430
+ }
431
+
432
+ /**
433
+ * Auto-create allowlist rule from a false positive.
434
+ */
435
+ autoAllowlist(feedbackId, options = {}) {
436
+ const entry = this.pendingReviews.find(e => e.id === feedbackId);
437
+ if (!entry || entry.type !== 'false_positive') return null;
438
+
439
+ const category = entry.threats.length > 0 ? entry.threats[0].category : null;
440
+
441
+ // Create a specific pattern from the text
442
+ const escapedText = entry.text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
443
+ const ruleId = this.allowlist.addRule({
444
+ pattern: options.pattern || escapedText.substring(0, 100),
445
+ category,
446
+ reason: options.reason || `Auto-allowlisted from feedback ${feedbackId}`,
447
+ addedBy: options.addedBy || 'feedback_loop'
448
+ });
449
+
450
+ entry.status = 'resolved';
451
+ entry.resolution = { action: 'allowlisted', ruleId };
452
+ return ruleId;
453
+ }
454
+
455
+ /**
456
+ * Get pending reviews.
457
+ */
458
+ getPendingReviews() {
459
+ return this.pendingReviews.filter(e => e.status === 'pending');
460
+ }
461
+
462
+ /**
463
+ * Get calibration suggestions.
464
+ */
465
+ getSuggestions() {
466
+ return this.calibrator.suggestThresholds();
467
+ }
468
+
469
+ /**
470
+ * Get stats.
471
+ */
472
+ getStats() {
473
+ return {
474
+ ...this.stats,
475
+ pending: this.pendingReviews.filter(e => e.status === 'pending').length,
476
+ calibration: this.calibrator.getMetrics()
477
+ };
478
+ }
479
+ }
480
+
481
+ // =========================================================================
482
+ // LRU Scan Cache
483
+ // =========================================================================
484
+
485
+ class ScanCache {
486
+ constructor(options = {}) {
487
+ this.maxSize = options.maxSize || 1000;
488
+ this.ttlMs = options.ttlMs || 60000; // 1 minute default
489
+ this.cache = new Map();
490
+ this.stats = { hits: 0, misses: 0, evictions: 0 };
491
+ }
492
+
493
+ /**
494
+ * Generate cache key from text and sensitivity.
495
+ */
496
+ _key(text, sensitivity) {
497
+ // Simple hash: use first 200 chars + length + sensitivity
498
+ const prefix = text.substring(0, 200);
499
+ return `${sensitivity}:${text.length}:${prefix}`;
500
+ }
501
+
502
+ /**
503
+ * Get a cached result.
504
+ */
505
+ get(text, sensitivity = 'high') {
506
+ const key = this._key(text, sensitivity);
507
+ const entry = this.cache.get(key);
508
+
509
+ if (!entry) {
510
+ this.stats.misses++;
511
+ return null;
512
+ }
513
+
514
+ // Check TTL
515
+ if (Date.now() - entry.timestamp > this.ttlMs) {
516
+ this.cache.delete(key);
517
+ this.stats.misses++;
518
+ return null;
519
+ }
520
+
521
+ // Move to end (LRU refresh)
522
+ this.cache.delete(key);
523
+ this.cache.set(key, entry);
524
+ this.stats.hits++;
525
+
526
+ return entry.result;
527
+ }
528
+
529
+ /**
530
+ * Store a result in cache.
531
+ */
532
+ set(text, sensitivity, result) {
533
+ const key = this._key(text, sensitivity);
534
+
535
+ // Evict oldest if at capacity
536
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
537
+ const firstKey = this.cache.keys().next().value;
538
+ this.cache.delete(firstKey);
539
+ this.stats.evictions++;
540
+ }
541
+
542
+ this.cache.set(key, {
543
+ result,
544
+ timestamp: Date.now()
545
+ });
546
+ }
547
+
548
+ /**
549
+ * Wrap a scan function with caching.
550
+ */
551
+ wrap(scanFn) {
552
+ return (text, sensitivity = 'high') => {
553
+ const cached = this.get(text, sensitivity);
554
+ if (cached) return { ...cached, _cached: true };
555
+
556
+ const result = scanFn(text, sensitivity);
557
+ this.set(text, sensitivity, result);
558
+ return result;
559
+ };
560
+ }
561
+
562
+ /**
563
+ * Get cache stats.
564
+ */
565
+ getStats() {
566
+ const total = this.stats.hits + this.stats.misses;
567
+ return {
568
+ ...this.stats,
569
+ size: this.cache.size,
570
+ maxSize: this.maxSize,
571
+ hitRate: total > 0 ? `${((this.stats.hits / total) * 100).toFixed(1)}%` : '0%'
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Clear the cache.
577
+ */
578
+ clear() {
579
+ this.cache.clear();
580
+ }
581
+
582
+ /**
583
+ * Prune expired entries.
584
+ */
585
+ prune() {
586
+ const now = Date.now();
587
+ let pruned = 0;
588
+ for (const [key, entry] of this.cache) {
589
+ if (now - entry.timestamp > this.ttlMs) {
590
+ this.cache.delete(key);
591
+ pruned++;
592
+ }
593
+ }
594
+ return pruned;
595
+ }
596
+ }
597
+
598
+ module.exports = {
599
+ Allowlist,
600
+ ConfidenceCalibrator,
601
+ FeedbackLoop,
602
+ ScanCache
603
+ };