@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
93 lines • 10.4 kB
JavaScript
/**
* Time-windowed event deduplication utility.
* Suppresses repeated identical events within a configurable window.
* Used by SecurityMonitor and SecurityTelemetry to prevent log flooding.
*
* @example
* ```ts
* const dedup = new EventDeduplicator(60_000, 500);
*
* if (!dedup.shouldSuppress('injection\0/api/query\0SQL detected')) {
* logger.warn('New security event');
* }
* // Same key within 60s → suppressed
* dedup.shouldSuppress('injection\0/api/query\0SQL detected'); // true
*
* // Check how much noise was filtered
* const stats = dedup.getStats();
* // { suppressedCount: 1, processedCount: 1, cacheSize: 1 }
* ```
*/
export class EventDeduplicator {
windowMs;
maxSize;
recentKeys = new Map();
_suppressedCount = 0;
_processedCount = 0;
constructor(windowMs = 60_000, maxSize = 500) {
this.windowMs = windowMs;
this.maxSize = maxSize;
}
/**
* Returns true if this key should be suppressed (duplicate within window).
* Returns false if this is a new event that should be processed.
*/
shouldSuppress(key) {
// NFC-normalize to ensure canonical Unicode equivalents deduplicate correctly
// (e.g. 'Café' decomposed vs composed). Uses String.normalize directly to
// avoid circular dependency with UnicodeValidator → SecurityMonitor → this.
// Falls back to raw key if normalize throws (e.g. mocked in tests).
try {
key = key.normalize('NFC');
}
catch { /* use raw key */ }
const now = Date.now();
const lastSeen = this.recentKeys.get(key);
if (lastSeen && (now - lastSeen) < this.windowMs) {
this._suppressedCount++;
return true;
}
this._processedCount++;
this.recentKeys.set(key, now);
try {
this.cleanup(now);
}
catch { /* eviction failure is non-fatal */ }
return false;
}
/** Clear all tracked keys and counters */
clear() {
this.recentKeys.clear();
this._suppressedCount = 0;
this._processedCount = 0;
}
get size() {
return this.recentKeys.size;
}
/** Returns deduplication statistics for metrics and observability */
getStats() {
return {
suppressedCount: this._suppressedCount,
processedCount: this._processedCount,
cacheSize: this.recentKeys.size,
};
}
cleanup(now) {
if (this.recentKeys.size <= this.maxSize)
return;
// First pass: evict expired entries
for (const [key, ts] of this.recentKeys) {
if ((now - ts) >= this.windowMs)
this.recentKeys.delete(key);
}
// Fallback: if still over capacity (all entries within window), evict oldest
if (this.recentKeys.size > this.maxSize) {
const entries = [...this.recentKeys.entries()].sort((a, b) => a[1] - b[1]);
const toRemove = entries.length - this.maxSize;
for (let i = 0; i < toRemove; i++) {
this.recentKeys.delete(entries[i][0]);
}
}
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRXZlbnREZWR1cGxpY2F0b3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdXRpbHMvRXZlbnREZWR1cGxpY2F0b3IudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7QUFDSCxNQUFNLE9BQU8saUJBQWlCO0lBTVQ7SUFDQTtJQU5GLFVBQVUsR0FBRyxJQUFJLEdBQUcsRUFBa0IsQ0FBQztJQUNoRCxnQkFBZ0IsR0FBRyxDQUFDLENBQUM7SUFDckIsZUFBZSxHQUFHLENBQUMsQ0FBQztJQUU1QixZQUNtQixXQUFtQixNQUFNLEVBQ3pCLFVBQWtCLEdBQUc7UUFEckIsYUFBUSxHQUFSLFFBQVEsQ0FBaUI7UUFDekIsWUFBTyxHQUFQLE9BQU8sQ0FBYztJQUNyQyxDQUFDO0lBRUo7OztPQUdHO0lBQ0gsY0FBYyxDQUFDLEdBQVc7UUFDeEIsOEVBQThFO1FBQzlFLDBFQUEwRTtRQUMxRSw0RUFBNEU7UUFDNUUsb0VBQW9FO1FBQ3BFLElBQUksQ0FBQztZQUFDLEdBQUcsR0FBRyxHQUFHLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQUMsQ0FBQztRQUFDLE1BQU0sQ0FBQyxDQUFDLGlCQUFpQixDQUFDLENBQUM7UUFDL0QsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRTFDLElBQUksUUFBUSxJQUFJLENBQUMsR0FBRyxHQUFHLFFBQVEsQ0FBQyxHQUFHLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNqRCxJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztZQUN4QixPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCxJQUFJLENBQUMsZUFBZSxFQUFFLENBQUM7UUFDdkIsSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxDQUFDO1FBQzlCLElBQUksQ0FBQztZQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUM7UUFBQyxDQUFDO1FBQUMsTUFBTSxDQUFDLENBQUMsbUNBQW1DLENBQUMsQ0FBQztRQUN4RSxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFFRCwwQ0FBMEM7SUFDMUMsS0FBSztRQUNILElBQUksQ0FBQyxVQUFVLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDeEIsSUFBSSxDQUFDLGdCQUFnQixHQUFHLENBQUMsQ0FBQztRQUMxQixJQUFJLENBQUMsZUFBZSxHQUFHLENBQUMsQ0FBQztJQUMzQixDQUFDO0lBRUQsSUFBSSxJQUFJO1FBQ04sT0FBTyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQztJQUM5QixDQUFDO0lBRUQscUVBQXFFO0lBQ3JFLFFBQVE7UUFDTixPQUFPO1lBQ0wsZUFBZSxFQUFFLElBQUksQ0FBQyxnQkFBZ0I7WUFDdEMsY0FBYyxFQUFFLElBQUksQ0FBQyxlQUFlO1lBQ3BDLFNBQVMsRUFBRSxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUk7U0FDaEMsQ0FBQztJQUNKLENBQUM7SUFFTyxPQUFPLENBQUMsR0FBVztRQUN6QixJQUFJLElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBQyxPQUFPO1lBQUUsT0FBTztRQUVqRCxvQ0FBb0M7UUFDcEMsS0FBSyxNQUFNLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxJQUFJLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztZQUN4QyxJQUFJLENBQUMsR0FBRyxHQUFHLEVBQUUsQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRO2dCQUFFLElBQUksQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQy9ELENBQUM7UUFFRCw2RUFBNkU7UUFDN0UsSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDeEMsTUFBTSxPQUFPLEdBQUcsQ0FBQyxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDM0UsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDO1lBQy9DLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxRQUFRLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztnQkFDbEMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDeEMsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFRpbWUtd2luZG93ZWQgZXZlbnQgZGVkdXBsaWNhdGlvbiB1dGlsaXR5LlxuICogU3VwcHJlc3NlcyByZXBlYXRlZCBpZGVudGljYWwgZXZlbnRzIHdpdGhpbiBhIGNvbmZpZ3VyYWJsZSB3aW5kb3cuXG4gKiBVc2VkIGJ5IFNlY3VyaXR5TW9uaXRvciBhbmQgU2VjdXJpdHlUZWxlbWV0cnkgdG8gcHJldmVudCBsb2cgZmxvb2RpbmcuXG4gKlxuICogQGV4YW1wbGVcbiAqIGBgYHRzXG4gKiBjb25zdCBkZWR1cCA9IG5ldyBFdmVudERlZHVwbGljYXRvcig2MF8wMDAsIDUwMCk7XG4gKlxuICogaWYgKCFkZWR1cC5zaG91bGRTdXBwcmVzcygnaW5qZWN0aW9uXFwwL2FwaS9xdWVyeVxcMFNRTCBkZXRlY3RlZCcpKSB7XG4gKiAgIGxvZ2dlci53YXJuKCdOZXcgc2VjdXJpdHkgZXZlbnQnKTtcbiAqIH1cbiAqIC8vIFNhbWUga2V5IHdpdGhpbiA2MHMg4oaSIHN1cHByZXNzZWRcbiAqIGRlZHVwLnNob3VsZFN1cHByZXNzKCdpbmplY3Rpb25cXDAvYXBpL3F1ZXJ5XFwwU1FMIGRldGVjdGVkJyk7IC8vIHRydWVcbiAqXG4gKiAvLyBDaGVjayBob3cgbXVjaCBub2lzZSB3YXMgZmlsdGVyZWRcbiAqIGNvbnN0IHN0YXRzID0gZGVkdXAuZ2V0U3RhdHMoKTtcbiAqIC8vIHsgc3VwcHJlc3NlZENvdW50OiAxLCBwcm9jZXNzZWRDb3VudDogMSwgY2FjaGVTaXplOiAxIH1cbiAqIGBgYFxuICovXG5leHBvcnQgY2xhc3MgRXZlbnREZWR1cGxpY2F0b3Ige1xuICBwcml2YXRlIHJlYWRvbmx5IHJlY2VudEtleXMgPSBuZXcgTWFwPHN0cmluZywgbnVtYmVyPigpO1xuICBwcml2YXRlIF9zdXBwcmVzc2VkQ291bnQgPSAwO1xuICBwcml2YXRlIF9wcm9jZXNzZWRDb3VudCA9IDA7XG5cbiAgY29uc3RydWN0b3IoXG4gICAgcHJpdmF0ZSByZWFkb25seSB3aW5kb3dNczogbnVtYmVyID0gNjBfMDAwLFxuICAgIHByaXZhdGUgcmVhZG9ubHkgbWF4U2l6ZTogbnVtYmVyID0gNTAwLFxuICApIHt9XG5cbiAgLyoqXG4gICAqIFJldHVybnMgdHJ1ZSBpZiB0aGlzIGtleSBzaG91bGQgYmUgc3VwcHJlc3NlZCAoZHVwbGljYXRlIHdpdGhpbiB3aW5kb3cpLlxuICAgKiBSZXR1cm5zIGZhbHNlIGlmIHRoaXMgaXMgYSBuZXcgZXZlbnQgdGhhdCBzaG91bGQgYmUgcHJvY2Vzc2VkLlxuICAgKi9cbiAgc2hvdWxkU3VwcHJlc3Moa2V5OiBzdHJpbmcpOiBib29sZWFuIHtcbiAgICAvLyBORkMtbm9ybWFsaXplIHRvIGVuc3VyZSBjYW5vbmljYWwgVW5pY29kZSBlcXVpdmFsZW50cyBkZWR1cGxpY2F0ZSBjb3JyZWN0bHlcbiAgICAvLyAoZS5nLiAnQ2Fmw6knIGRlY29tcG9zZWQgdnMgY29tcG9zZWQpLiBVc2VzIFN0cmluZy5ub3JtYWxpemUgZGlyZWN0bHkgdG9cbiAgICAvLyBhdm9pZCBjaXJjdWxhciBkZXBlbmRlbmN5IHdpdGggVW5pY29kZVZhbGlkYXRvciDihpIgU2VjdXJpdHlNb25pdG9yIOKGkiB0aGlzLlxuICAgIC8vIEZhbGxzIGJhY2sgdG8gcmF3IGtleSBpZiBub3JtYWxpemUgdGhyb3dzIChlLmcuIG1vY2tlZCBpbiB0ZXN0cykuXG4gICAgdHJ5IHsga2V5ID0ga2V5Lm5vcm1hbGl6ZSgnTkZDJyk7IH0gY2F0Y2ggeyAvKiB1c2UgcmF3IGtleSAqLyB9XG4gICAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgICBjb25zdCBsYXN0U2VlbiA9IHRoaXMucmVjZW50S2V5cy5nZXQoa2V5KTtcblxuICAgIGlmIChsYXN0U2VlbiAmJiAobm93IC0gbGFzdFNlZW4pIDwgdGhpcy53aW5kb3dNcykge1xuICAgICAgdGhpcy5fc3VwcHJlc3NlZENvdW50Kys7XG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG5cbiAgICB0aGlzLl9wcm9jZXNzZWRDb3VudCsrO1xuICAgIHRoaXMucmVjZW50S2V5cy5zZXQoa2V5LCBub3cpO1xuICAgIHRyeSB7IHRoaXMuY2xlYW51cChub3cpOyB9IGNhdGNoIHsgLyogZXZpY3Rpb24gZmFpbHVyZSBpcyBub24tZmF0YWwgKi8gfVxuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIC8qKiBDbGVhciBhbGwgdHJhY2tlZCBrZXlzIGFuZCBjb3VudGVycyAqL1xuICBjbGVhcigpOiB2b2lkIHtcbiAgICB0aGlzLnJlY2VudEtleXMuY2xlYXIoKTtcbiAgICB0aGlzLl9zdXBwcmVzc2VkQ291bnQgPSAwO1xuICAgIHRoaXMuX3Byb2Nlc3NlZENvdW50ID0gMDtcbiAgfVxuXG4gIGdldCBzaXplKCk6IG51bWJlciB7XG4gICAgcmV0dXJuIHRoaXMucmVjZW50S2V5cy5zaXplO1xuICB9XG5cbiAgLyoqIFJldHVybnMgZGVkdXBsaWNhdGlvbiBzdGF0aXN0aWNzIGZvciBtZXRyaWNzIGFuZCBvYnNlcnZhYmlsaXR5ICovXG4gIGdldFN0YXRzKCk6IHsgc3VwcHJlc3NlZENvdW50OiBudW1iZXI7IHByb2Nlc3NlZENvdW50OiBudW1iZXI7IGNhY2hlU2l6ZTogbnVtYmVyIH0ge1xuICAgIHJldHVybiB7XG4gICAgICBzdXBwcmVzc2VkQ291bnQ6IHRoaXMuX3N1cHByZXNzZWRDb3VudCxcbiAgICAgIHByb2Nlc3NlZENvdW50OiB0aGlzLl9wcm9jZXNzZWRDb3VudCxcbiAgICAgIGNhY2hlU2l6ZTogdGhpcy5yZWNlbnRLZXlzLnNpemUsXG4gICAgfTtcbiAgfVxuXG4gIHByaXZhdGUgY2xlYW51cChub3c6IG51bWJlcik6IHZvaWQge1xuICAgIGlmICh0aGlzLnJlY2VudEtleXMuc2l6ZSA8PSB0aGlzLm1heFNpemUpIHJldHVybjtcblxuICAgIC8vIEZpcnN0IHBhc3M6IGV2aWN0IGV4cGlyZWQgZW50cmllc1xuICAgIGZvciAoY29uc3QgW2tleSwgdHNdIG9mIHRoaXMucmVjZW50S2V5cykge1xuICAgICAgaWYgKChub3cgLSB0cykgPj0gdGhpcy53aW5kb3dNcykgdGhpcy5yZWNlbnRLZXlzLmRlbGV0ZShrZXkpO1xuICAgIH1cblxuICAgIC8vIEZhbGxiYWNrOiBpZiBzdGlsbCBvdmVyIGNhcGFjaXR5IChhbGwgZW50cmllcyB3aXRoaW4gd2luZG93KSwgZXZpY3Qgb2xkZXN0XG4gICAgaWYgKHRoaXMucmVjZW50S2V5cy5zaXplID4gdGhpcy5tYXhTaXplKSB7XG4gICAgICBjb25zdCBlbnRyaWVzID0gWy4uLnRoaXMucmVjZW50S2V5cy5lbnRyaWVzKCldLnNvcnQoKGEsIGIpID0+IGFbMV0gLSBiWzFdKTtcbiAgICAgIGNvbnN0IHRvUmVtb3ZlID0gZW50cmllcy5sZW5ndGggLSB0aGlzLm1heFNpemU7XG4gICAgICBmb3IgKGxldCBpID0gMDsgaSA8IHRvUmVtb3ZlOyBpKyspIHtcbiAgICAgICAgdGhpcy5yZWNlbnRLZXlzLmRlbGV0ZShlbnRyaWVzW2ldWzBdKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cbiJdfQ==