UNPKG

express-rate-limiter-ts

Version:

A powerful, flexible and modern rate limiter middleware for Express.js written in TypeScript.

152 lines (151 loc) 7.01 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rateLimiter = rateLimiter; exports.getRateLimiterStats = getRateLimiterStats; const memoryStore_1 = require("./storage/memoryStore"); function isInList(list, req, key) { if (!list) return false; if (Array.isArray(list)) return list.includes(key); if (typeof list === 'function') return list(req); return false; } // Helper function: Set rate limit headers function setRateLimitHeaders(res, limit, remaining, resetTime) { // Classic headers res.setHeader('X-RateLimit-Limit', limit); res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining)); res.setHeader('X-RateLimit-Reset', resetTime.getTime()); // IETF draft headers res.setHeader('RateLimit-Limit', limit); res.setHeader('RateLimit-Remaining', Math.max(0, remaining)); // Reset in seconds (RFC proposal) const resetSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1000); res.setHeader('RateLimit-Reset', resetSeconds > 0 ? resetSeconds : 0); } function rateLimiter(options, store) { // --- Configuration and environment checks --- if (!options.windowMs || typeof options.windowMs !== 'number' || options.windowMs <= 0) { console.warn('[express-rate-limiter-ts] Invalid or missing windowMs!'); } if (!options.max || typeof options.max !== 'number' || options.max <= 0) { console.warn('[express-rate-limiter-ts] Invalid or missing max!'); } if (options.identifierType && !['ip', 'header', 'jwt'].includes(options.identifierType)) { console.warn('[express-rate-limiter-ts] Invalid identifierType:', options.identifierType); } if (options.identifierType === 'header' && !options.identifierHeader) { console.warn('[express-rate-limiter-ts] identifierType header selected but identifierHeader not specified. Default x-api-key will be used.'); } // MemoryStore production warning if (!store && process.env.NODE_ENV === 'production') { console.warn('[express-rate-limiter-ts] WARNING: MemoryStore is not recommended in production! Data loss and inconsistency may occur in distributed systems.'); } // Trust proxy info (informational) if (process.env.EXPRESS_RATE_LIMITER_LOG_PROXY !== 'off') { console.info('[express-rate-limiter-ts] Info: If your app is running behind a proxy, don\'t forget to configure Express trust proxy setting.'); } const { windowMs, max, message = 'Too many requests', statusCode = 429, keyGenerator, skip, logger, routes = {}, identifierType = 'ip', identifierHeader = 'x-api-key', stats = false, whitelist, blacklist, customResponse, banSeconds = 0, dynamicMax, } = options; const usedStore = store || new memoryStore_1.MemoryStore(windowMs); return async function (req, res, next) { var _a, _b, _c; // Apply route-specific options const routeKey = ((_a = req.route) === null || _a === void 0 ? void 0 : _a.path) || req.path; const routeOptions = routes[routeKey] || {}; const mergedOptions = { ...options, ...routeOptions }; if (mergedOptions.skip && mergedOptions.skip(req)) { next(); return; } // Update stats on every request if (stats) await usedStore.recordStat(routeKey); // Key generation let key = ''; if (mergedOptions.identifierType === 'header') { const headerName = mergedOptions.identifierHeader || 'x-api-key'; key = req.header(headerName) || req.ip || ''; } else if (mergedOptions.identifierType === 'jwt') { const auth = req.header('authorization') || ''; const token = auth.split(' ')[1]; try { const payload = token ? JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()) : {}; key = payload.userId || payload.sub || req.ip || ''; } catch { key = req.ip || ''; } } else { key = req.ip || ''; } if (mergedOptions.keyGenerator) { key = mergedOptions.keyGenerator(req); } // Whitelist check if (isInList(mergedOptions.whitelist, req, key)) { next(); return; } // Blacklist check if (isInList(mergedOptions.blacklist, req, key)) { (_b = logger === null || logger === void 0 ? void 0 : logger.warn) === null || _b === void 0 ? void 0 : _b.call(logger, `Blacklisted key: ${key}`, { route: routeKey, key }); res.status(403).json({ success: false, message: 'Access denied', key }); return; } // Ban check const banInfo = await usedStore.isBanned(key); if (banInfo.banned) { res.status(429).json({ success: false, message: 'You have been temporarily blocked due to too many requests', banExpiresAt: banInfo.banExpiresAt, reason: banInfo.reason, }); return; } // Dynamic max const effectiveMax = typeof mergedOptions.dynamicMax === 'function' ? mergedOptions.dynamicMax(req) : mergedOptions.max; // Rate limiting process const rateInfo = await usedStore.incr(key); rateInfo.exceeded = rateInfo.current > effectiveMax; // Remaining requests and limit check if (rateInfo.exceeded) { (_c = logger === null || logger === void 0 ? void 0 : logger.warn) === null || _c === void 0 ? void 0 : _c.call(logger, `Rate limit exceeded for key: ${key}`, { route: routeKey, key }); // Apply ban if (banSeconds > 0) { await usedStore.ban(key, banSeconds, 'Limit exceeded by a large margin'); rateInfo.banned = true; rateInfo.banExpiresAt = new Date(Date.now() + banSeconds * 1000); } // Custom response if (typeof mergedOptions.customResponse === 'function') { mergedOptions.customResponse(req, res, rateInfo); return; } setRateLimitHeaders(res, effectiveMax, 0, rateInfo.resetTime); res.status(mergedOptions.statusCode || 429).json({ success: false, message: mergedOptions.message, limit: effectiveMax, current: rateInfo.current, resetTime: rateInfo.resetTime, banned: rateInfo.banned, banExpiresAt: rateInfo.banExpiresAt, }); return; } setRateLimitHeaders(res, effectiveMax, effectiveMax - rateInfo.current, rateInfo.resetTime); next(); }; } // Statistics and ban list functions for admin panel: function getRateLimiterStats(store) { return { stats: store.getStats(), banned: store.getBanned(), }; }