@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
JavaScript
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