import { logger } from '../../logger.js'; /** * Rate limiter using token bucket algorithm * Provides more sophisticated rate limiting with burst handling */ export class RateLimiter { /** Rate limit configuration */ config; /** Token buckets per key */ buckets = new Map(); /** Global bucket for non-keyed rate limiting */ globalBucket; /** * Create a new rate limiter * @param config Rate limiter configuration */ constructor(config) { // Set defaults this.config = { maxPerPeriod: config.maxPerPeriod, periodMs: config.periodMs, perKey: config.perKey ?? true, initialTokens: config.initialTokens ?? config.maxPerPeriod, burstTokens: config.burstTokens ?? 0, useGlobalLimit: config.useGlobalLimit ?? false }; // Initialize global bucket this.globalBucket = { tokens: this.config.initialTokens, lastRefill: Date.now(), allowed: 0, denied: 0, errors: 0, firstErrorTime: 0 }; // Log initialization logger.log('info', `Rate limiter initialized: ${this.config.maxPerPeriod} per ${this.config.periodMs}ms${this.config.perKey ? ' per key' : ''}`); } /** * Check if a request is allowed under rate limits * @param key Key to check rate limit for (e.g. domain, user, IP) * @param cost Token cost (defaults to 1) * @returns Whether the request is allowed */ isAllowed(key = 'global', cost = 1) { // If using global bucket directly, just check that if (key === 'global' || !this.config.perKey) { return this.checkBucket(this.globalBucket, cost); } // Get the key-specific bucket const bucket = this.getBucket(key); // If we also need to check global limit if (this.config.useGlobalLimit) { // Both key bucket and global bucket must have tokens return this.checkBucket(bucket, cost) && this.checkBucket(this.globalBucket, cost); } else { // Only need to check the key-specific bucket return this.checkBucket(bucket, cost); } } /** * Check if a bucket has enough tokens and consume them * @param bucket The token bucket to check * @param cost Token cost * @returns Whether tokens were consumed */ checkBucket(bucket, cost) { // Refill tokens based on elapsed time this.refillBucket(bucket); // Check if we have enough tokens if (bucket.tokens >= cost) { // Use tokens bucket.tokens -= cost; bucket.allowed++; return true; } else { // Rate limit exceeded bucket.denied++; return false; } } /** * Consume tokens for a request (if available) * @param key Key to consume tokens for * @param cost Token cost (defaults to 1) * @returns Whether tokens were consumed */ consume(key = 'global', cost = 1) { const isAllowed = this.isAllowed(key, cost); return isAllowed; } /** * Get the remaining tokens for a key * @param key Key to check * @returns Number of remaining tokens */ getRemainingTokens(key = 'global') { const bucket = this.getBucket(key); this.refillBucket(bucket); return bucket.tokens; } /** * Get stats for a specific key * @param key Key to get stats for * @returns Rate limit statistics */ getStats(key = 'global') { const bucket = this.getBucket(key); this.refillBucket(bucket); // Calculate time until next token const resetIn = bucket.tokens < this.config.maxPerPeriod ? Math.ceil(this.config.periodMs / this.config.maxPerPeriod) : 0; return { remaining: bucket.tokens, limit: this.config.maxPerPeriod, resetIn, allowed: bucket.allowed, denied: bucket.denied }; } /** * Get or create a token bucket for a key * @param key The rate limit key * @returns Token bucket */ getBucket(key) { if (!this.config.perKey || key === 'global') { return this.globalBucket; } if (!this.buckets.has(key)) { // Create new bucket this.buckets.set(key, { tokens: this.config.initialTokens, lastRefill: Date.now(), allowed: 0, denied: 0, errors: 0, firstErrorTime: 0 }); } return this.buckets.get(key); } /** * Refill tokens in a bucket based on elapsed time * @param bucket Token bucket to refill */ refillBucket(bucket) { const now = Date.now(); const elapsedMs = now - bucket.lastRefill; // Calculate how many tokens to add const rate = this.config.maxPerPeriod / this.config.periodMs; const tokensToAdd = elapsedMs * rate; if (tokensToAdd >= 0.1) { // Allow for partial token refills // Add tokens, but don't exceed the normal maximum (without burst) // This ensures burst tokens are only used for bursts and don't refill const normalMax = this.config.maxPerPeriod; bucket.tokens = Math.min( // Don't exceed max + burst this.config.maxPerPeriod + (this.config.burstTokens || 0), // Don't exceed normal max when refilling Math.min(normalMax, bucket.tokens + tokensToAdd)); // Update last refill time bucket.lastRefill = now; } } /** * Reset rate limits for a specific key * @param key Key to reset */ reset(key = 'global') { if (key === 'global' || !this.config.perKey) { this.globalBucket.tokens = this.config.initialTokens; this.globalBucket.lastRefill = Date.now(); } else if (this.buckets.has(key)) { const bucket = this.buckets.get(key); bucket.tokens = this.config.initialTokens; bucket.lastRefill = Date.now(); } } /** * Reset all rate limiters */ resetAll() { this.globalBucket.tokens = this.config.initialTokens; this.globalBucket.lastRefill = Date.now(); for (const bucket of this.buckets.values()) { bucket.tokens = this.config.initialTokens; bucket.lastRefill = Date.now(); } } /** * Cleanup old buckets to prevent memory leaks * @param maxAge Maximum age in milliseconds */ cleanup(maxAge = 24 * 60 * 60 * 1000) { const now = Date.now(); let removed = 0; for (const [key, bucket] of this.buckets.entries()) { if (now - bucket.lastRefill > maxAge) { this.buckets.delete(key); removed++; } } if (removed > 0) { logger.log('debug', `Cleaned up ${removed} stale rate limit buckets`); } } /** * Record an error for a key (e.g., IP address) and determine if blocking is needed * RFC 5321 Section 4.5.4.1 suggests limiting errors to prevent abuse * * @param key Key to record error for (typically an IP address) * @param errorWindow Time window for error tracking in ms (default: 5 minutes) * @param errorThreshold Maximum errors before blocking (default: 10) * @returns true if the key should be blocked due to excessive errors */ recordError(key, errorWindow = 5 * 60 * 1000, errorThreshold = 10) { const bucket = this.getBucket(key); const now = Date.now(); // Reset error count if the time window has expired if (bucket.firstErrorTime === 0 || now - bucket.firstErrorTime > errorWindow) { bucket.errors = 0; bucket.firstErrorTime = now; } // Increment error count bucket.errors++; // Log error tracking logger.log('debug', `Error recorded for ${key}: ${bucket.errors}/${errorThreshold} in window`); // Check if threshold exceeded if (bucket.errors >= errorThreshold) { logger.log('warn', `Error threshold exceeded for ${key}: ${bucket.errors} errors`); return true; // Should block } return false; // Continue allowing } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5yYXRlbGltaXRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvY2xhc3Nlcy5yYXRlbGltaXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFnRHpDOzs7R0FHRztBQUNILE1BQU0sT0FBTyxXQUFXO0lBQ3RCLCtCQUErQjtJQUN2QixNQUFNLENBQW1CO0lBRWpDLDRCQUE0QjtJQUNwQixPQUFPLEdBQTZCLElBQUksR0FBRyxFQUFFLENBQUM7SUFFdEQsZ0RBQWdEO0lBQ3hDLFlBQVksQ0FBYztJQUVsQzs7O09BR0c7SUFDSCxZQUFZLE1BQXdCO1FBQ2xDLGVBQWU7UUFDZixJQUFJLENBQUMsTUFBTSxHQUFHO1lBQ1osWUFBWSxFQUFFLE1BQU0sQ0FBQyxZQUFZO1lBQ2pDLFFBQVEsRUFBRSxNQUFNLENBQUMsUUFBUTtZQUN6QixNQUFNLEVBQUUsTUFBTSxDQUFDLE1BQU0sSUFBSSxJQUFJO1lBQzdCLGFBQWEsRUFBRSxNQUFNLENBQUMsYUFBYSxJQUFJLE1BQU0sQ0FBQyxZQUFZO1lBQzFELFdBQVcsRUFBRSxNQUFNLENBQUMsV0FBVyxJQUFJLENBQUM7WUFDcEMsY0FBYyxFQUFFLE1BQU0sQ0FBQyxjQUFjLElBQUksS0FBSztTQUMvQyxDQUFDO1FBRUYsMkJBQTJCO1FBQzNCLElBQUksQ0FBQyxZQUFZLEdBQUc7WUFDbEIsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYTtZQUNqQyxVQUFVLEVBQUUsSUFBSSxDQUFDLEdBQUcsRUFBRTtZQUN0QixPQUFPLEVBQUUsQ0FBQztZQUNWLE1BQU0sRUFBRSxDQUFDO1lBQ1QsTUFBTSxFQUFFLENBQUM7WUFDVCxjQUFjLEVBQUUsQ0FBQztTQUNsQixDQUFDO1FBRUYscUJBQXFCO1FBQ3JCLE1BQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLDZC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