UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

307 lines (306 loc) 10.9 kB
/** * Rate Limiting Implementation for RuVector * * Provides distributed rate limiting with: * - Token bucket algorithm for fair rate limiting * - Per-user/per-service rate limits * - Burst capacity support * - Multiple time windows (per-minute, per-hour, per-day) * - Redis-backed distributed state (optional) * - In-memory fallback for single-instance deployments * * Security Features: * - Prevents brute force attacks * - DoS attack mitigation * - Fair resource allocation * - Configurable per-actor limits * - Non-blocking failure modes * * CVSS Mitigation: Prevents resource exhaustion and DoS attacks * Compliance: Supports OWASP Rate Limiting Best Practices */ import { createLogger } from './logging.js'; const logger = createLogger('rate-limiter'); /** * In-memory rate limit store */ export class RateLimiter { /** Rate limit configurations per actor */ configs = new Map(); /** Token buckets: actor_id -> { minute, hour, day } */ buckets = new Map(); /** Redis client for distributed rate limiting (optional) */ redisClient; /** Cleanup interval reference */ cleanupInterval = null; constructor(redisClient){ this.redisClient = redisClient; // Start cleanup of old buckets periodically (every 5 minutes) this.cleanupInterval = setInterval(()=>{ this.cleanupOldBuckets(); }, 5 * 60 * 1000); logger.info('Rate limiter initialized', { distributed: !!redisClient }); } /** * Set rate limit configuration for an actor */ setConfig(actor_id, config) { this.configs.set(actor_id, config); // Clear cached buckets for this actor this.buckets.delete(actor_id); logger.debug('Rate limit config set', { actor_id, config }); } /** * Check if request is allowed under rate limits */ async checkLimit(actor_id, config) { try { if (this.redisClient) { return await this.checkLimitRedis(actor_id, config); } else { return this.checkLimitMemory(actor_id, config); } } catch (error) { logger.error('Rate limit check failed', { error, actor_id }); // On error, allow request (fail open for availability) return { allowed: true, remaining: config.per_minute, reset_at: new Date(Date.now() + 60000) }; } } /** * In-memory rate limit check using token bucket algorithm */ checkLimitMemory(actor_id, config) { const now = Date.now(); // Get or create buckets for this actor let actorBuckets = this.buckets.get(actor_id); if (!actorBuckets) { actorBuckets = new Map(); this.buckets.set(actor_id, actorBuckets); } // Check per-minute limit const minuteBucket = this.getOrCreateBucket(actorBuckets, 'minute', config.per_minute, 60000); this.refillBucket(minuteBucket, now); if (minuteBucket.tokens < 1) { const resetAt = new Date(minuteBucket.last_refill + minuteBucket.window_ms); return { allowed: false, remaining: 0, reset_at: resetAt, retry_after_seconds: Math.ceil((resetAt.getTime() - now) / 1000) }; } // Consume token minuteBucket.tokens--; // Check per-hour limit const hourBucket = this.getOrCreateBucket(actorBuckets, 'hour', config.per_hour, 3600000); this.refillBucket(hourBucket, now); if (hourBucket.tokens < 1) { const resetAt = new Date(hourBucket.last_refill + hourBucket.window_ms); return { allowed: false, remaining: 0, reset_at: resetAt, retry_after_seconds: Math.ceil((resetAt.getTime() - now) / 1000) }; } hourBucket.tokens--; // Check per-day limit const dayBucket = this.getOrCreateBucket(actorBuckets, 'day', config.per_day, 86400000); this.refillBucket(dayBucket, now); if (dayBucket.tokens < 1) { const resetAt = new Date(dayBucket.last_refill + dayBucket.window_ms); return { allowed: false, remaining: 0, reset_at: resetAt, retry_after_seconds: Math.ceil((resetAt.getTime() - now) / 1000) }; } dayBucket.tokens--; logger.debug('Rate limit check passed', { actor_id, remaining_minute: Math.floor(minuteBucket.tokens) }); return { allowed: true, remaining: Math.floor(minuteBucket.tokens), reset_at: new Date(minuteBucket.last_refill + minuteBucket.window_ms) }; } /** * Redis-based distributed rate limit check */ async checkLimitRedis(actor_id, config) { if (!this.redisClient) { return this.checkLimitMemory(actor_id, config); } const now = Date.now(); const keys = { minute: `rl:${actor_id}:minute`, hour: `rl:${actor_id}:hour`, day: `rl:${actor_id}:day` }; try { // Increment minute counter with TTL const minuteKey = keys.minute; const minuteCount = await this.redisClient.incr(minuteKey); if (minuteCount === 1) { await this.redisClient.expire(minuteKey, 60); } if (minuteCount > config.per_minute) { const ttl = await this.redisClient.ttl(minuteKey); return { allowed: false, remaining: 0, reset_at: new Date(now + ttl * 1000), retry_after_seconds: ttl }; } // Increment hour counter with TTL const hourKey = keys.hour; const hourCount = await this.redisClient.incr(hourKey); if (hourCount === 1) { await this.redisClient.expire(hourKey, 3600); } if (hourCount > config.per_hour) { const ttl = await this.redisClient.ttl(hourKey); return { allowed: false, remaining: 0, reset_at: new Date(now + ttl * 1000), retry_after_seconds: ttl }; } // Increment day counter with TTL const dayKey = keys.day; const dayCount = await this.redisClient.incr(dayKey); if (dayCount === 1) { await this.redisClient.expire(dayKey, 86400); } if (dayCount > config.per_day) { const ttl = await this.redisClient.ttl(dayKey); return { allowed: false, remaining: 0, reset_at: new Date(now + ttl * 1000), retry_after_seconds: ttl }; } return { allowed: true, remaining: Math.max(0, config.per_minute - minuteCount), reset_at: new Date(now + 60000) }; } catch (error) { logger.error('Redis rate limit check failed', { error, actor_id }); // Fail open on error return { allowed: true, remaining: config.per_minute, reset_at: new Date(now + 60000) }; } } /** * Get or create a rate limit bucket */ getOrCreateBucket(buckets, window, limit, window_ms) { let bucket = buckets.get(window); if (!bucket) { const now = Date.now(); bucket = { tokens: limit, last_refill: now, window_ms, refill_rate: limit / window_ms }; buckets.set(window, bucket); } return bucket; } /** * Refill bucket with tokens based on time elapsed */ refillBucket(bucket, now) { const elapsed = now - bucket.last_refill; const tokensToAdd = elapsed * bucket.refill_rate; bucket.tokens = Math.min(bucket.tokens + tokensToAdd, bucket.window_ms * bucket.refill_rate); bucket.last_refill = now; } /** * Clean up old buckets from memory */ cleanupOldBuckets() { const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours let removed = 0; for (const [actor_id, buckets] of this.buckets){ for (const [window, bucket] of buckets){ if (now - bucket.last_refill > maxAge) { buckets.delete(window); removed++; } } // Remove actor entry if no buckets if (buckets.size === 0) { this.buckets.delete(actor_id); } } if (removed > 0) { logger.debug('Cleaned up old rate limit buckets', { removed }); } } /** * Get statistics about rate limiting */ getStats() { const stats = { tracked_actors: this.buckets.size, configured_actors: this.configs.size, memory_usage_estimate_bytes: 0 }; // Estimate memory usage stats.memory_usage_estimate_bytes = stats.tracked_actors * 200; // ~200 bytes per actor return stats; } /** * Reset rate limit for an actor (admin operation) */ async resetLimit(actor_id) { this.buckets.delete(actor_id); if (this.redisClient) { try { await this.redisClient.del(`rl:${actor_id}:minute`, `rl:${actor_id}:hour`, `rl:${actor_id}:day`); } catch (error) { logger.error('Failed to reset Redis rate limits', { error, actor_id }); } } logger.info('Rate limit reset for actor', { actor_id }); } /** * Shutdown and cleanup */ shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.buckets.clear(); logger.info('Rate limiter shut down'); } } /** * Create a singleton rate limiter instance */ let rateLimiterInstance = null; export function getRateLimiter(redisClient) { if (!rateLimiterInstance) { rateLimiterInstance = new RateLimiter(redisClient); } return rateLimiterInstance; } //# sourceMappingURL=rate-limiter.js.map