UNPKG

endpoint-sentinel

Version:

User-friendly security scanner with interactive setup that scales from beginner to expert

232 lines 8.14 kB
"use strict"; /** * Production-grade Token Bucket Rate Limiter * Implements ethical scanning boundaries with configurable limits */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RateLimiterFactory = exports.AdaptiveRateLimiter = exports.TokenBucketRateLimiter = void 0; class TokenBucketRateLimiter { requestsPerSecond; burstSize; tokens; lastRefill; totalRequests = 0; throttledRequests = 0; constructor(requestsPerSecond = 2, burstSize = 5) { this.requestsPerSecond = requestsPerSecond; this.burstSize = burstSize; this.tokens = this.burstSize; this.lastRefill = new Date(); } /** * Throttles requests according to token bucket algorithm * Respects server capabilities and ethical boundaries */ async throttle(_request) { this.refillTokens(); this.totalRequests++; if (this.tokens >= 1) { this.tokens--; return; // Request can proceed immediately } // Need to wait for tokens to be available this.throttledRequests++; const waitTime = this.calculateWaitTime(); if (waitTime > 0) { await this.sleep(waitTime); this.refillTokens(); this.tokens = Math.max(0, this.tokens - 1); } } /** * Refills tokens based on elapsed time */ refillTokens() { const now = new Date(); const elapsed = (now.getTime() - this.lastRefill.getTime()) / 1000; const tokensToAdd = elapsed * this.requestsPerSecond; this.tokens = Math.min(this.burstSize, this.tokens + tokensToAdd); this.lastRefill = now; } /** * Calculates wait time until next token is available */ calculateWaitTime() { if (this.tokens >= 1) return 0; const tokensNeeded = 1 - this.tokens; return (tokensNeeded / this.requestsPerSecond) * 1000; // Convert to milliseconds } /** * Sleep utility for throttling */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Gets current rate limiting statistics */ getStats() { this.refillTokens(); // Update tokens before returning stats return { requestsPerSecond: this.requestsPerSecond, burstSize: this.burstSize, tokensAvailable: this.tokens, lastRefill: this.lastRefill, totalRequests: this.totalRequests, throttledRequests: this.throttledRequests }; } /** * Updates rate limiting parameters */ updateLimits(requestsPerSecond, burstSize) { if (requestsPerSecond <= 0 || burstSize <= 0) { throw new Error('Rate limit parameters must be positive'); } this.requestsPerSecond = requestsPerSecond; this.burstSize = burstSize; this.tokens = Math.min(this.tokens, this.burstSize); } /** * Calculates optimal rate limit based on server response headers */ adaptToServerCapabilities(responseHeaders) { // Check for server-provided rate limit hints const retryAfter = responseHeaders['retry-after']; const rateLimitRemaining = responseHeaders['x-ratelimit-remaining']; const rateLimitReset = responseHeaders['x-ratelimit-reset']; if (retryAfter) { const retrySeconds = parseInt(retryAfter, 10); if (!isNaN(retrySeconds) && retrySeconds > 0) { // Temporarily reduce rate if server indicates we should wait this.updateLimits(Math.min(0.5, this.requestsPerSecond), this.burstSize); } } if (rateLimitRemaining && rateLimitReset) { const remaining = parseInt(rateLimitRemaining, 10); const resetTime = parseInt(rateLimitReset, 10); if (!isNaN(remaining) && !isNaN(resetTime) && remaining < 10) { // Slow down if we're approaching rate limits this.updateLimits(this.requestsPerSecond * 0.5, this.burstSize); } } } /** * Resets rate limiter state */ reset() { this.tokens = this.burstSize; this.lastRefill = new Date(); this.totalRequests = 0; this.throttledRequests = 0; } } exports.TokenBucketRateLimiter = TokenBucketRateLimiter; /** * Advanced Rate Limiter with domain-specific controls */ class AdaptiveRateLimiter extends TokenBucketRateLimiter { domainLimits = new Map(); serverResponseTimes = new Map(); constructor(requestsPerSecond = 2, burstSize = 5) { super(requestsPerSecond, burstSize); } /** * Sets domain-specific rate limits */ setDomainLimit(domain, requestsPerSecond, burstSize) { this.domainLimits.set(domain, { requestsPerSecond, burstSize }); } /** * Throttles with domain-aware rate limiting */ async throttle(request) { const domain = this.extractDomain(request.url); const domainLimits = this.domainLimits.get(domain); if (domainLimits) { const originalRps = this.requestsPerSecond; const originalBurst = this.burstSize; this.updateLimits(domainLimits.requestsPerSecond, domainLimits.burstSize); await super.throttle(request); // Restore original limits this.updateLimits(originalRps, originalBurst); } else { await super.throttle(request); } } /** * Records server response time for adaptive rate limiting */ recordResponseTime(url, responseTime) { const domain = this.extractDomain(url); const times = this.serverResponseTimes.get(domain) || []; times.push(responseTime); // Keep only last 10 response times if (times.length > 10) { times.shift(); } this.serverResponseTimes.set(domain, times); this.adaptRateToServerPerformance(domain); } /** * Adapts rate limiting based on server performance */ adaptRateToServerPerformance(domain) { const times = this.serverResponseTimes.get(domain); if (!times || times.length < 3) return; const avgResponseTime = times.reduce((sum, time) => sum + time, 0) / times.length; let adaptedRate = this.requestsPerSecond; if (avgResponseTime > 5000) { // Slow server (>5s) adaptedRate = Math.max(0.5, this.requestsPerSecond * 0.3); } else if (avgResponseTime > 2000) { // Moderate server (>2s) adaptedRate = Math.max(1, this.requestsPerSecond * 0.6); } else if (avgResponseTime < 500) { // Fast server (<500ms) adaptedRate = Math.min(5, this.requestsPerSecond * 1.5); } this.setDomainLimit(domain, adaptedRate, this.burstSize); } /** * Extracts domain from URL */ extractDomain(url) { try { return new URL(url).hostname; } catch { return 'unknown'; } } } exports.AdaptiveRateLimiter = AdaptiveRateLimiter; /** * Rate Limiter Factory */ class RateLimiterFactory { static createBasic(requestsPerSecond = 2, burstSize = 5) { return new TokenBucketRateLimiter(requestsPerSecond, burstSize); } static createAdaptive(requestsPerSecond = 2, burstSize = 5) { return new AdaptiveRateLimiter(requestsPerSecond, burstSize); } static createEthical() { // Conservative settings for ethical scanning return new TokenBucketRateLimiter(1, 3); } static createFromConfig(config) { switch (config.type) { case 'adaptive': return this.createAdaptive(config.requestsPerSecond, config.burstSize); case 'ethical': return this.createEthical(); default: return this.createBasic(config.requestsPerSecond, config.burstSize); } } } exports.RateLimiterFactory = RateLimiterFactory; //# sourceMappingURL=rate-limiter.js.map