UNPKG

anon-identity

Version:

Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure

624 lines 20.5 kB
"use strict"; /** * Enhanced Rate Limiter and Quota Manager for MCP * * Implements sophisticated rate limiting and quota management for LLM interactions */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RateLimiterManager = exports.RateLimitWindow = void 0; const events_1 = require("events"); /** * Rate limit window types */ var RateLimitWindow; (function (RateLimitWindow) { RateLimitWindow[RateLimitWindow["SECOND"] = 1000] = "SECOND"; RateLimitWindow[RateLimitWindow["MINUTE"] = 60000] = "MINUTE"; RateLimitWindow[RateLimitWindow["HOUR"] = 3600000] = "HOUR"; RateLimitWindow[RateLimitWindow["DAY"] = 86400000] = "DAY"; RateLimitWindow[RateLimitWindow["WEEK"] = 604800000] = "WEEK"; RateLimitWindow[RateLimitWindow["MONTH"] = 2592000000] = "MONTH"; })(RateLimitWindow || (exports.RateLimitWindow = RateLimitWindow = {})); /** * Enhanced Rate Limiter and Quota Manager */ class RateLimiterManager extends events_1.EventEmitter { constructor() { super(); this.rules = new Map(); this.quotas = new Map(); this.tokenBuckets = new Map(); this.usageHistory = []; this.slidingWindows = new Map(); this.cooldowns = new Map(); this.initializeDefaultRules(); this.startCleanupTimer(); } /** * Initialize default rate limit rules */ initializeDefaultRules() { // Global rate limits this.addRule({ id: 'global-requests-per-second', name: 'Global requests per second', window: RateLimitWindow.SECOND, limit: 10, scope: 'global', priority: 100, burst: 20 }); this.addRule({ id: 'global-requests-per-minute', name: 'Global requests per minute', window: RateLimitWindow.MINUTE, limit: 100, scope: 'global', priority: 90 }); // Per-agent rate limits this.addRule({ id: 'agent-requests-per-minute', name: 'Agent requests per minute', window: RateLimitWindow.MINUTE, limit: 60, scope: 'agent', priority: 80 }); this.addRule({ id: 'agent-requests-per-hour', name: 'Agent requests per hour', window: RateLimitWindow.HOUR, limit: 1000, scope: 'agent', priority: 70 }); // Per-provider rate limits this.addRule({ id: 'provider-requests-per-minute', name: 'Provider requests per minute', window: RateLimitWindow.MINUTE, limit: 50, scope: 'provider', priority: 60 }); } /** * Check rate limit */ async checkRateLimit(request, provider, model) { const agentDID = request.agentDID; const violatedRules = []; let mostRestrictive = null; // Check all applicable rules const applicableRules = this.getApplicableRules(request, provider, model); for (const rule of applicableRules) { const key = this.getRateLimitKey(rule, agentDID, provider, model); const result = this.checkRule(rule, key); if (!result.allowed) { violatedRules.push(rule); // Track most restrictive rule if (!mostRestrictive || (result.retryAfter || 0) > (mostRestrictive.retryAfter || 0)) { mostRestrictive = result; } } } // If any rule violated, return the most restrictive if (mostRestrictive) { return { ...mostRestrictive, violatedRules }; } // All rules passed - consume tokens for (const rule of applicableRules) { const key = this.getRateLimitKey(rule, agentDID, provider, model); this.consumeToken(rule, key); } return { allowed: true }; } /** * Check quota */ async checkQuota(agentDID, usage, provider, model) { const applicableQuotas = this.getApplicableQuotas(agentDID, provider, model); for (const quota of applicableQuotas) { // Check if quota would be exceeded const wouldExceed = this.wouldExceedQuota(quota, usage); if (wouldExceed && quota.hardLimit) { return { allowed: false, reason: `Quota exceeded: ${quota.name}`, resetAt: quota.resetAt, retryAfter: Math.ceil((quota.resetAt.getTime() - Date.now()) / 1000) }; } // Check warning threshold if (quota.warningThreshold) { const percentageAfter = this.calculateQuotaPercentage(quota, usage); if (percentageAfter >= quota.warningThreshold) { this.emit('quota_warning', { quota, percentageUsed: percentageAfter, agentDID }); } } } return { allowed: true }; } /** * Record usage */ async recordUsage(agentDID, usage, provider, model) { // Record in history const entry = { timestamp: Date.now(), agentDID, provider, model, requests: 1, tokens: usage.totalTokens || 0, cost: usage.cost || 0 }; this.usageHistory.push(entry); // Update quotas const applicableQuotas = this.getApplicableQuotas(agentDID, provider, model); for (const quota of applicableQuotas) { quota.usage.requests += 1; quota.usage.tokens += usage.totalTokens || 0; quota.usage.cost += usage.cost || 0; // Check if quota exceeded if (this.isQuotaExceeded(quota)) { this.emit('quota_exceeded', { quota, agentDID, provider, model }); } } // Emit usage event this.emit('usage_recorded', entry); } /** * Add rate limit rule */ addRule(rule) { this.rules.set(rule.id, rule); // Initialize token bucket if using burst if (rule.burst) { const key = this.getRateLimitKey(rule, '*', '*', '*'); this.tokenBuckets.set(key, { tokens: rule.burst, capacity: rule.burst, refillRate: rule.limit / (rule.window / 1000), lastRefill: Date.now() }); } } /** * Remove rate limit rule */ removeRule(ruleId) { this.rules.delete(ruleId); } /** * Add quota */ addQuota(quota) { // Set reset time quota.resetAt = new Date(Date.now() + quota.window); // Initialize usage if not set if (!quota.usage) { quota.usage = { requests: 0, tokens: 0, cost: 0 }; } this.quotas.set(quota.id, quota); // Schedule quota reset this.scheduleQuotaReset(quota); } /** * Remove quota */ removeQuota(quotaId) { this.quotas.delete(quotaId); } /** * Get quota status */ getQuotaStatus(quotaId) { const quota = this.quotas.get(quotaId); if (!quota) return null; const percentageUsed = this.calculateQuotaPercentage(quota); const isWarning = quota.warningThreshold ? percentageUsed >= quota.warningThreshold : false; const isExceeded = this.isQuotaExceeded(quota); return { quota, percentageUsed, remaining: { requests: quota.limits.requests ? Math.max(0, quota.limits.requests - quota.usage.requests) : undefined, tokens: quota.limits.tokens ? Math.max(0, quota.limits.tokens - quota.usage.tokens) : undefined, cost: quota.limits.cost ? Math.max(0, quota.limits.cost - quota.usage.cost) : undefined }, willReset: quota.resetAt, isWarning, isExceeded }; } /** * Get all quota statuses for agent */ getAgentQuotaStatuses(agentDID) { const statuses = []; for (const quota of this.quotas.values()) { if (!quota.agentDID || quota.agentDID === agentDID) { const status = this.getQuotaStatus(quota.id); if (status) { statuses.push(status); } } } return statuses; } /** * Get usage statistics */ getUsageStatistics(window, agentDID, provider) { const cutoff = Date.now() - window; const relevantEntries = this.usageHistory.filter(entry => { if (entry.timestamp < cutoff) return false; if (agentDID && entry.agentDID !== agentDID) return false; if (provider && entry.provider !== provider) return false; return true; }); const stats = { requests: 0, tokens: 0, cost: 0, averageTokensPerRequest: 0, peakRequestsPerMinute: 0 }; // Sum up usage for (const entry of relevantEntries) { stats.requests += entry.requests; stats.tokens += entry.tokens; stats.cost += entry.cost; } // Calculate averages if (stats.requests > 0) { stats.averageTokensPerRequest = stats.tokens / stats.requests; } // Calculate peak requests per minute const minuteBuckets = new Map(); for (const entry of relevantEntries) { const minute = Math.floor(entry.timestamp / 60000); minuteBuckets.set(minute, (minuteBuckets.get(minute) || 0) + entry.requests); } stats.peakRequestsPerMinute = Math.max(...Array.from(minuteBuckets.values()), 0); return stats; } /** * Get applicable rules */ getApplicableRules(request, provider, model) { const rules = Array.from(this.rules.values()); return rules .filter(rule => { // Check resource match if (rule.resource && rule.resource !== request.type) return false; // Check action match if (rule.action && rule.action !== request.type) return false; return true; }) .sort((a, b) => b.priority - a.priority); } /** * Get applicable quotas */ getApplicableQuotas(agentDID, provider, model) { const quotas = []; for (const quota of this.quotas.values()) { // Check if quota applies if (quota.agentDID && quota.agentDID !== agentDID) continue; if (quota.provider && quota.provider !== provider) continue; if (quota.model && quota.model !== model) continue; quotas.push(quota); } return quotas; } /** * Get rate limit key */ getRateLimitKey(rule, agentDID, provider, model) { const parts = [`rule:${rule.id}`]; switch (rule.scope) { case 'global': parts.push('global'); break; case 'agent': parts.push(`agent:${agentDID}`); break; case 'provider': parts.push(`provider:${provider || 'unknown'}`); break; case 'model': parts.push(`model:${model || 'unknown'}`); break; } return parts.join(':'); } /** * Check rule using sliding window */ checkRule(rule, key) { // Check cooldown first const cooldownUntil = this.cooldowns.get(key); if (cooldownUntil && cooldownUntil > Date.now()) { return { allowed: false, limit: rule.limit, remaining: 0, retryAfter: Math.ceil((cooldownUntil - Date.now()) / 1000), reason: `Rate limit cooldown for ${rule.name}` }; } // Use token bucket if configured if (rule.burst) { return this.checkTokenBucket(rule, key); } // Use sliding window const now = Date.now(); const window = rule.window; const windowStart = now - window; // Get or create sliding window let timestamps = this.slidingWindows.get(key) || []; // Remove old entries timestamps = timestamps.filter(t => t > windowStart); this.slidingWindows.set(key, timestamps); // Check if limit exceeded if (timestamps.length >= rule.limit) { const oldestTimestamp = timestamps[0]; const resetAt = new Date(oldestTimestamp + window); // Apply cooldown if configured if (rule.cooldown) { this.cooldowns.set(key, now + rule.cooldown); } return { allowed: false, limit: rule.limit, remaining: 0, resetAt, retryAfter: Math.ceil((resetAt.getTime() - now) / 1000), reason: `Rate limit exceeded for ${rule.name}` }; } return { allowed: true, limit: rule.limit, remaining: rule.limit - timestamps.length }; } /** * Check token bucket */ checkTokenBucket(rule, key) { let bucket = this.tokenBuckets.get(key); if (!bucket) { bucket = { tokens: rule.burst, capacity: rule.burst, refillRate: rule.limit / (rule.window / 1000), lastRefill: Date.now() }; this.tokenBuckets.set(key, bucket); } // Refill tokens const now = Date.now(); const timePassed = (now - bucket.lastRefill) / 1000; const tokensToAdd = timePassed * bucket.refillRate; bucket.tokens = Math.min(bucket.capacity, bucket.tokens + tokensToAdd); bucket.lastRefill = now; // Check if token available if (bucket.tokens < 1) { const timeToNextToken = (1 - bucket.tokens) / bucket.refillRate; return { allowed: false, limit: rule.limit, remaining: Math.floor(bucket.tokens), retryAfter: Math.ceil(timeToNextToken), reason: `Rate limit exceeded for ${rule.name} (token bucket)` }; } return { allowed: true, limit: rule.limit, remaining: Math.floor(bucket.tokens) }; } /** * Consume token */ consumeToken(rule, key) { if (rule.burst) { // Consume from token bucket const bucket = this.tokenBuckets.get(key); if (bucket) { bucket.tokens = Math.max(0, bucket.tokens - 1); } } else { // Add to sliding window const timestamps = this.slidingWindows.get(key) || []; timestamps.push(Date.now()); this.slidingWindows.set(key, timestamps); } } /** * Calculate quota percentage used */ calculateQuotaPercentage(quota, additionalUsage) { let percentage = 0; let count = 0; if (quota.limits.requests) { const used = quota.usage.requests + (additionalUsage ? 1 : 0); percentage += (used / quota.limits.requests) * 100; count++; } if (quota.limits.tokens) { const used = quota.usage.tokens + (additionalUsage?.totalTokens || 0); percentage += (used / quota.limits.tokens) * 100; count++; } if (quota.limits.cost) { const used = quota.usage.cost + (additionalUsage?.cost || 0); percentage += (used / quota.limits.cost) * 100; count++; } return count > 0 ? percentage / count : 0; } /** * Check if quota would be exceeded */ wouldExceedQuota(quota, usage) { if (quota.limits.requests && quota.usage.requests + 1 > quota.limits.requests) { return true; } if (quota.limits.tokens && quota.usage.tokens + (usage.totalTokens || 0) > quota.limits.tokens) { return true; } if (quota.limits.cost && quota.usage.cost + (usage.cost || 0) > quota.limits.cost) { return true; } return false; } /** * Check if quota is exceeded */ isQuotaExceeded(quota) { if (quota.limits.requests && quota.usage.requests > quota.limits.requests) { return true; } if (quota.limits.tokens && quota.usage.tokens > quota.limits.tokens) { return true; } if (quota.limits.cost && quota.usage.cost > quota.limits.cost) { return true; } return false; } /** * Schedule quota reset */ scheduleQuotaReset(quota) { const timeUntilReset = quota.resetAt.getTime() - Date.now(); if (timeUntilReset > 0) { setTimeout(() => { this.resetQuota(quota); }, timeUntilReset); } } /** * Reset quota */ resetQuota(quota) { quota.usage = { requests: 0, tokens: 0, cost: 0 }; quota.resetAt = new Date(Date.now() + quota.window); this.emit('quota_reset', quota); // Schedule next reset this.scheduleQuotaReset(quota); } /** * Start cleanup timer */ startCleanupTimer() { this.cleanupInterval = setInterval(() => { this.cleanup(); }, 60000); // Run every minute } /** * Clean up old data */ cleanup() { const now = Date.now(); // Clean old usage history (keep 24 hours) const historyAge = 24 * 60 * 60 * 1000; this.usageHistory = this.usageHistory.filter(entry => entry.timestamp > now - historyAge); // Clean expired cooldowns for (const [key, expiry] of this.cooldowns.entries()) { if (expiry < now) { this.cooldowns.delete(key); } } // Clean old sliding window entries for (const [key, timestamps] of this.slidingWindows.entries()) { // Keep entries for the longest window (1 month) const maxAge = RateLimitWindow.MONTH; const filtered = timestamps.filter(t => t > now - maxAge); if (filtered.length === 0) { this.slidingWindows.delete(key); } else if (filtered.length < timestamps.length) { this.slidingWindows.set(key, filtered); } } } /** * Export rate limit configuration */ exportConfiguration() { return { rules: Array.from(this.rules.values()), quotas: Array.from(this.quotas.values()) }; } /** * Import rate limit configuration */ importConfiguration(config) { if (config.rules) { for (const rule of config.rules) { this.addRule(rule); } } if (config.quotas) { for (const quota of config.quotas) { this.addQuota(quota); } } } /** * Shutdown rate limiter */ shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.rules.clear(); this.quotas.clear(); this.tokenBuckets.clear(); this.usageHistory = []; this.slidingWindows.clear(); this.cooldowns.clear(); this.removeAllListeners(); } } exports.RateLimiterManager = RateLimiterManager; exports.default = RateLimiterManager; //# sourceMappingURL=rate-limiter.js.map