Files
blackcanyontickets/reactrebuild0825/src/features/scanner/RateLimiter.ts
dzinesco aa81eb5adb feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 09:25:10 -06:00

265 lines
6.6 KiB
TypeScript

/**
* 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<RateLimiterConfig> = {}) {
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;
}
}