UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

290 lines (289 loc) 8.97 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../core/monitoring/logger.js"; import { metrics } from "../core/monitoring/metrics.js"; class ExponentialRateLimiter { redis; localCache = /* @__PURE__ */ new Map(); localCacheOrder = []; config; constructor(redis, config = {}) { this.redis = redis; this.config = { baseLimit: 10, windowMs: 60 * 1e3, // 1 minute maxBackoff: 32, backoffMultiplier: 2, localCacheSize: 1e4, localCacheTTL: 5 * 60 * 1e3, // 5 minutes whitelistIPs: [], blacklistIPs: [], customKeyGenerator: (req) => this.getClientIdentifier(req), ...config }; setInterval(() => this.cleanupLocalCache(), this.config.localCacheTTL); } /** * Main middleware function with exponential backoff */ middleware() { return async (req, res, next) => { const clientId = this.config.customKeyGenerator(req); if (this.isWhitelisted(clientId)) { return next(); } if (this.isBlacklisted(clientId)) { metrics.increment("rate_limit.blacklisted", { ip: clientId }); res.status(403).json({ error: "Access denied", code: "BLACKLISTED_IP" }); return; } try { let entry = this.getFromLocalCache(clientId); if (!entry) { entry = await this.getFromRedis(clientId); } const now = Date.now(); if (entry.blockedUntil && entry.blockedUntil > now) { const retryAfter = Math.ceil((entry.blockedUntil - now) / 1e3); metrics.increment("rate_limit.blocked", { ip: clientId, backoffLevel: String(entry.backoffLevel) }); res.status(429).json({ error: "Too many requests - exponential backoff applied", code: "RATE_LIMIT_BACKOFF", retryAfter, backoffLevel: entry.backoffLevel }); res.setHeader("Retry-After", String(retryAfter)); res.setHeader("X-RateLimit-BackoffLevel", String(entry.backoffLevel)); return; } if (now - entry.firstRequest > this.config.windowMs) { entry = { requests: 1, violations: Math.max(0, entry.violations - 1), // Decay violations backoffLevel: Math.max(0, entry.backoffLevel - 1), // Decay backoff firstRequest: now, lastRequest: now }; } else { entry.requests++; entry.lastRequest = now; } const currentLimit = Math.max( 1, Math.floor( this.config.baseLimit / Math.pow(this.config.backoffMultiplier, entry.backoffLevel) ) ); if (entry.requests > currentLimit) { entry.violations++; if (entry.backoffLevel < Math.log2(this.config.maxBackoff)) { entry.backoffLevel++; } const backoffDuration = this.config.windowMs * Math.pow(this.config.backoffMultiplier, entry.backoffLevel); entry.blockedUntil = now + backoffDuration; await this.updateCaches(clientId, entry); const retryAfter = Math.ceil(backoffDuration / 1e3); metrics.increment("rate_limit.exceeded", { ip: clientId, violations: String(entry.violations), backoffLevel: String(entry.backoffLevel) }); res.status(429).json({ error: "Rate limit exceeded - entering exponential backoff", code: "RATE_LIMIT_EXCEEDED", retryAfter, violations: entry.violations, backoffLevel: entry.backoffLevel, currentLimit }); res.setHeader("Retry-After", String(retryAfter)); res.setHeader("X-RateLimit-Limit", String(currentLimit)); res.setHeader("X-RateLimit-Remaining", "0"); res.setHeader("X-RateLimit-BackoffLevel", String(entry.backoffLevel)); return; } await this.updateCaches(clientId, entry); res.setHeader("X-RateLimit-Limit", String(currentLimit)); res.setHeader( "X-RateLimit-Remaining", String(currentLimit - entry.requests) ); res.setHeader( "X-RateLimit-Reset", String(new Date(entry.firstRequest + this.config.windowMs).getTime()) ); if (entry.backoffLevel > 0) { res.setHeader("X-RateLimit-BackoffLevel", String(entry.backoffLevel)); } next(); } catch (error) { logger.error( "Rate limiter error", error instanceof Error ? error : new Error(String(error)) ); next(); } }; } /** * Get client identifier from request */ getClientIdentifier(req) { const forwarded = req.headers["x-forwarded-for"]; const realIp = req.headers["x-real-ip"]; const cfIp = req.headers["cf-connecting-ip"]; if (typeof forwarded === "string") { return forwarded.split(",")[0].trim(); } if (typeof realIp === "string") { return realIp; } if (typeof cfIp === "string") { return cfIp; } return req.ip || req.socket.remoteAddress || "unknown"; } /** * Check if IP is whitelisted */ isWhitelisted(ip) { return this.config.whitelistIPs.includes(ip) || ip === "127.0.0.1" || ip === "::1" || ip.startsWith("192.168.") || ip.startsWith("10."); } /** * Check if IP is blacklisted */ isBlacklisted(ip) { return this.config.blacklistIPs.includes(ip); } /** * Get rate limit entry from local cache */ getFromLocalCache(clientId) { const cached = this.localCache.get(clientId); if (cached) { const now = Date.now(); if (now - cached.lastRequest < this.config.localCacheTTL) { return cached; } this.localCache.delete(clientId); const index = this.localCacheOrder.indexOf(clientId); if (index > -1) { this.localCacheOrder.splice(index, 1); } } return null; } /** * Get rate limit entry from Redis */ async getFromRedis(clientId) { const key = `rate_limit:${clientId}`; const data = await this.redis.get(key); if (data) { return JSON.parse(data); } return { requests: 0, violations: 0, backoffLevel: 0, firstRequest: Date.now(), lastRequest: Date.now() }; } /** * Update both local cache and Redis */ async updateCaches(clientId, entry) { if (!this.localCache.has(clientId)) { if (this.localCache.size >= this.config.localCacheSize) { const oldest = this.localCacheOrder.shift(); if (oldest) { this.localCache.delete(oldest); } } this.localCacheOrder.push(clientId); } this.localCache.set(clientId, entry); const key = `rate_limit:${clientId}`; const ttl = Math.ceil( this.config.windowMs * Math.pow(2, entry.backoffLevel) / 1e3 ); await this.redis.setex(key, ttl, JSON.stringify(entry)); } /** * Clean up stale entries from local cache */ cleanupLocalCache() { const now = Date.now(); const staleThreshold = now - this.config.localCacheTTL; for (const [clientId, entry] of this.localCache.entries()) { if (entry.lastRequest < staleThreshold) { this.localCache.delete(clientId); const index = this.localCacheOrder.indexOf(clientId); if (index > -1) { this.localCacheOrder.splice(index, 1); } } } metrics.record("rate_limit.local_cache_size", this.localCache.size); } /** * Reset rate limit for a specific client */ async reset(clientId) { this.localCache.delete(clientId); const index = this.localCacheOrder.indexOf(clientId); if (index > -1) { this.localCacheOrder.splice(index, 1); } await this.redis.del(`rate_limit:${clientId}`); } /** * Get current rate limit status for a client */ async getStatus(clientId) { let entry = this.getFromLocalCache(clientId); if (!entry) { const data = await this.redis.get(`rate_limit:${clientId}`); if (data) { entry = JSON.parse(data); } } return entry; } /** * Add IP to blacklist */ blacklistIP(ip) { if (!this.config.blacklistIPs.includes(ip)) { this.config.blacklistIPs.push(ip); logger.warn("IP blacklisted", { ip }); } } /** * Remove IP from blacklist */ unblacklistIP(ip) { const index = this.config.blacklistIPs.indexOf(ip); if (index > -1) { this.config.blacklistIPs.splice(index, 1); logger.info("IP unblacklisted", { ip }); } } } export { ExponentialRateLimiter }; //# sourceMappingURL=exponential-rate-limiter.js.map