"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InMemoryStore = void 0;
exports.createRateLimiter = createRateLimiter;
const store_1 = require("./store");
/**
* Default logger that wraps console methods.
*/
const createDefaultLogger = () => ({
log: (...args) => console.log(...args),
warn: (...args) => console.warn(...args),
error: (...args) => console.error(...args),
});
/**
* Create an Express rate limiter middleware.
*/
function createRateLimiter(config) {
const { algorithm = 'sliding-window', windowMs, maxRequests, tokenBucket, keyGenerator = defaultKeyGenerator, skip, onLimitReached, onBlocked, headers: headersConfig = false, store = new store_1.InMemoryStore(windowMs), metrics, logger = createDefaultLogger(), } = config;
const headersEnabled = typeof headersConfig === 'boolean' ? headersConfig : headersConfig.enabled;
const retryAfterHeader = typeof headersConfig === 'object' ? headersConfig.retryAfterHeader !== false : true;
const customHeaders = typeof headersConfig === 'object' ? headersConfig.customHeaders : undefined;
const bucketSize = tokenBucket?.bucketSize ?? maxRequests;
const refillRate = tokenBucket?.refillRate ?? (maxRequests * 1000) / windowMs;
const limitReachedClients = new Set();
return async (req, res, next) => {
try {
if (skip && skip(req)) {
return next();
}
const key = keyGenerator(req);
const now = Date.now();
const entry = await store.get(key);
let info;
let isBlocked = false;
if (algorithm === 'token-bucket') {
const { bucketEntry, allowed } = createOrUpdateTokenBucketEntry(entry, now, bucketSize, refillRate);
await store.set(key, bucketEntry);
const remainingRequests = Math.max(0, Math.floor(bucketEntry.tokens));
const resetInMs = allowed
? 0
: Math.ceil((1 - bucketEntry.tokens) / refillRate * 1000);
const currentRequests = bucketSize - remainingRequests;
info = {
key,
windowMs,
maxRequests: bucketSize,
currentRequests,
remainingRequests,
resetInMs,
};
if (!allowed) {
isBlocked = true;
}
}
else {
const windowEntry = createOrUpdateSlidingWindowEntry(entry, now, windowMs);
await store.set(key, windowEntry);
const currentRequests = windowEntry.timestamps.length;
const remainingRequests = Math.max(0, maxRequests - currentRequests);
const resetInMs = currentRequests === 0
? windowMs
: Math.max(0, windowMs - (now - windowEntry.timestamps[0]));
info = {
key,
windowMs,
maxRequests,
currentRequests,
remainingRequests,
resetInMs,
};
if (currentRequests > maxRequests) {
isBlocked = true;
}
}
if (headersEnabled) {
setRateLimitHeaders(res, info, retryAfterHeader, customHeaders);
}
if (isBlocked) {
if (!limitReachedClients.has(key)) {
limitReachedClients.add(key);
if (onLimitReached) {
onLimitReached(req, res, info);
}
}
if (onBlocked) {
onBlocked(req, res, info);
}
else {
res.status(429).json({
error: 'Too Many Requests',
retryAfter: Math.ceil(info.resetInMs / 1000),
});
}
if (metrics?.recordBlocked) {
metrics.recordBlocked(req, info);
}
return;
}
if (metrics?.recordAllowed) {
metrics.recordAllowed(req, info);
}
if (metrics?.recordCurrentUsage) {
metrics.recordCurrentUsage(req, info);
}
if (info.remainingRequests > 0 && limitReachedClients.has(key)) {
limitReachedClients.delete(key);
}
next();
}
catch (error) {
logger.error('Rate limiter error:', error);
next();
}
};
}
/**
* Default key generator using client IP address.
*/
function defaultKeyGenerator(req) {
return req.ip || 'unknown';
}
function createOrUpdateSlidingWindowEntry(entry, now, windowMs) {
const windowStart = now - windowMs;
const currentEntry = entry && entry.type === 'sliding-window'
? entry
: { type: 'sliding-window', timestamps: [], lastAccessedAt: now };
currentEntry.timestamps = currentEntry.timestamps.filter((timestamp) => timestamp > windowStart);
currentEntry.timestamps.push(now);
currentEntry.lastAccessedAt = now;
return currentEntry;
}
function createOrUpdateTokenBucketEntry(entry, now, bucketSize, refillRate) {
const currentEntry = entry && entry.type === 'token-bucket'
? entry
: {
type: 'token-bucket',
tokens: bucketSize,
lastRefillAt: now,
bucketSize,
refillRate,
lastAccessedAt: now,
};
const elapsedMs = now - currentEntry.lastRefillAt;
const refillTokens = (elapsedMs / 1000) * currentEntry.refillRate;
const availableTokens = Math.min(currentEntry.bucketSize, currentEntry.tokens + refillTokens);
const allowed = availableTokens >= 1;
currentEntry.tokens = allowed ? availableTokens - 1 : availableTokens;
currentEntry.lastRefillAt = now;
currentEntry.lastAccessedAt = now;
currentEntry.bucketSize = bucketSize;
currentEntry.refillRate = refillRate;
return { bucketEntry: currentEntry, allowed };
}
function setRateLimitHeaders(res, info, retryAfterHeader, customHeaders) {
res.setHeader('X-RateLimit-Limit', info.maxRequests);
res.setHeader('X-RateLimit-Remaining', info.remainingRequests);
res.setHeader('X-RateLimit-Reset', Math.ceil((Date.now() + info.resetInMs) / 1000));
if (retryAfterHeader && info.remainingRequests === 0) {
res.setHeader('Retry-After', Math.ceil(info.resetInMs / 1000));
}
if (customHeaders) {
for (const [name, value] of Object.entries(customHeaders)) {
res.setHeader(name, String(value));
}
}
}
var store_2 = require("./store");
Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return store_2.InMemoryStore; } });
__exportStar(require("./types"), exports);
//# sourceMappingURL=index.js.map