/** * Rate Limiter for Scanner Abuse Prevention * * Implements sliding window rate limiting to prevent scan spam * and ensure stable gate operations. */ export interface RateLimitResult { allowed: boolean; waitTime?: number; // milliseconds until next scan allowed message?: string; currentRate: number; // scans per second in current window } export interface RateLimiterConfig { maxScansPerSecond: number; windowSizeMs: number; cooldownMs: number; // Additional cooldown when limit exceeded } export class ScannerRateLimiter { private scanTimestamps: number[] = []; private lastViolationTime: number | null = null; private violationCount = 0; private readonly config: RateLimiterConfig; constructor(config: Partial = {}) { this.config = { maxScansPerSecond: 8, windowSizeMs: 1000, cooldownMs: 2000, ...config, }; } /** * Check if a scan is allowed at the current time */ checkRate(now = Date.now()): RateLimitResult { this.cleanOldTimestamps(now); const currentRate = this.getCurrentRate(now); const inCooldown = this.isInCooldown(now); if (inCooldown) { const remainingCooldown = this.getRemainingCooldown(now); return { allowed: false, waitTime: remainingCooldown, message: `Cooling down - wait ${Math.ceil(remainingCooldown / 1000)}s`, currentRate, }; } if (currentRate >= this.config.maxScansPerSecond) { this.recordViolation(now); return { allowed: false, waitTime: this.config.cooldownMs, message: 'Scanning too fast - slow down', currentRate, }; } return { allowed: true, currentRate, }; } /** * Record a successful scan */ recordScan(now = Date.now()): void { this.scanTimestamps.push(now); this.cleanOldTimestamps(now); } /** * Get current scans per second rate */ getScansInWindow(now = Date.now()): number { this.cleanOldTimestamps(now); return this.scanTimestamps.length; } /** * Get current rate as scans per second */ private getCurrentRate(now: number): number { const scansInWindow = this.getScansInWindow(now); return scansInWindow * (1000 / this.config.windowSizeMs); } /** * Remove timestamps outside the sliding window */ private cleanOldTimestamps(now: number): void { const cutoff = now - this.config.windowSizeMs; this.scanTimestamps = this.scanTimestamps.filter(time => time > cutoff); } /** * Check if currently in cooldown period */ private isInCooldown(now: number): boolean { if (!this.lastViolationTime) {return false;} return (now - this.lastViolationTime) < this.config.cooldownMs; } /** * Get remaining cooldown time in milliseconds */ private getRemainingCooldown(now: number): number { if (!this.lastViolationTime) {return 0;} const elapsed = now - this.lastViolationTime; return Math.max(0, this.config.cooldownMs - elapsed); } /** * Record a rate limit violation */ private recordViolation(now: number): void { this.lastViolationTime = now; this.violationCount++; } /** * Get statistics for monitoring */ getStats(now = Date.now()): { currentRate: number; scansInWindow: number; violationCount: number; inCooldown: boolean; remainingCooldown: number; } { return { currentRate: this.getCurrentRate(now), scansInWindow: this.getScansInWindow(now), violationCount: this.violationCount, inCooldown: this.isInCooldown(now), remainingCooldown: this.getRemainingCooldown(now), }; } /** * Reset rate limiter state (for testing or manual reset) */ reset(): void { this.scanTimestamps = []; this.lastViolationTime = null; this.violationCount = 0; } } /** * Device-level abuse tracker * Tracks suspicious patterns and implements exponential backoff */ export class DeviceAbuseTracker { private violations = 0; private lastViolationTime: number | null = null; private suspiciousPatterns = 0; private readonly deviceFingerprint: string; constructor() { this.deviceFingerprint = this.generateFingerprint(); } /** * Generate device fingerprint for tracking */ private generateFingerprint(): string { const components = [ navigator.userAgent.split(' ').slice(0, 3).join('_'), // Simplified user agent `${screen.width }x${ screen.height}`, Intl.DateTimeFormat().resolvedOptions().timeZone, navigator.language, ]; // Simple hash of components let hash = 0; const str = components.join('|'); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(36); } /** * Record a rate limit violation */ recordViolation(now = Date.now()): void { this.violations++; this.lastViolationTime = now; } /** * Check for suspicious patterns and apply exponential backoff */ checkAbuseStatus(now = Date.now()): { isAbusive: boolean; backoffTime: number; message?: string; } { // No violations recorded if (this.violations === 0) { return { isAbusive: false, backoffTime: 0 }; } // Calculate exponential backoff based on violation count const baseBackoff = 5000; // 5 seconds base backoff const backoffMultiplier = Math.pow(2, Math.min(this.violations - 1, 6)); // Cap at 2^6 = 64x const backoffTime = baseBackoff * backoffMultiplier; // Check if still in backoff period if (this.lastViolationTime && (now - this.lastViolationTime) < backoffTime) { const remainingTime = backoffTime - (now - this.lastViolationTime); return { isAbusive: true, backoffTime: remainingTime, message: `Device blocked - wait ${Math.ceil(remainingTime / 1000)}s`, }; } return { isAbusive: false, backoffTime: 0 }; } /** * Record suspicious scanning pattern */ recordSuspiciousPattern(): void { this.suspiciousPatterns++; } /** * Get device abuse statistics */ getStats(): { deviceId: string; violations: number; suspiciousPatterns: number; lastViolation: number | null; } { return { deviceId: this.deviceFingerprint, violations: this.violations, suspiciousPatterns: this.suspiciousPatterns, lastViolation: this.lastViolationTime, }; } /** * Reset abuse tracking (for testing or manual reset) */ reset(): void { this.violations = 0; this.lastViolationTime = null; this.suspiciousPatterns = 0; } }