- 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>
265 lines
6.6 KiB
TypeScript
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;
|
|
}
|
|
} |