/**
 * Rate Limiting Middleware
 *
 * Implements sliding window rate limiting with support for:
 * - Global rate limits
 * - Per-key rate limits
 * - Per-endpoint rate limits
 * - Route-based configuration
 */
import { API_ERRORS } from '../types.js';
/**
 * Default rate limit configurations by route prefix
 */
export const DEFAULT_ROUTE_LIMITS = {
    '/api/': {
        maxRequests: 100,
        windowMs: 60000, // 1 minute
        keyPrefix: 'api',
    },
    '/auth/': {
        maxRequests: 10,
        windowMs: 60000, // 1 minute
        keyPrefix: 'auth',
    },
    '/v1/': {
        maxRequests: 100,
        windowMs: 60000, // 1 minute
        keyPrefix: 'v1',
    },
};
// In-memory store for rate limits with max size to prevent unbounded growth
const MAX_RATE_LIMIT_ENTRIES = 10000;
const rateLimitStore = new Map();
// Cleanup old entries periodically + evict oldest if over max size
const rateLimitCleanupTimer = setInterval(() => {
    const now = Date.now();
    for (const [key, entry] of rateLimitStore.entries()) {
        if (entry.resetAt < now) {
            rateLimitStore.delete(key);
        }
    }
    // LRU eviction: if still over max, remove oldest entries (Map preserves insertion order)
    if (rateLimitStore.size > MAX_RATE_LIMIT_ENTRIES) {
        const excess = rateLimitStore.size - MAX_RATE_LIMIT_ENTRIES;
        const keys = rateLimitStore.keys();
        for (let i = 0; i < excess; i++) {
            const next = keys.next();
            if (!next.done)
                rateLimitStore.delete(next.value);
        }
    }
}, 60000); // Cleanup every minute
rateLimitCleanupTimer.unref();
/**
 * Get rate limit key for request
 */
function getRateLimitKey(req, prefix) {
    let baseKey;
    // Use API key ID if authenticated
    if (req.auth?.keyId) {
        baseKey = `key:${req.auth.keyId}`;
    }
    else if (req.auth?.userId) {
        // Use user ID if authenticated
        baseKey = `user:${req.auth.userId}`;
    }
    else {
        // Fall back to IP address
        const ip = req.ip || req.socket.remoteAddress || 'unknown';
        baseKey = `ip:${ip}`;
    }
    return prefix ? `${prefix}:${baseKey}` : baseKey;
}
/**
 * Find matching route configuration for a request path
 */
function findRouteConfig(path, routeLimits) {
    // Sort by prefix length (longest first) for most specific match
    const sortedPrefixes = Object.keys(routeLimits).sort((a, b) => b.length - a.length);
    for (const prefix of sortedPrefixes) {
        if (path.startsWith(prefix)) {
            return routeLimits[prefix];
        }
    }
    return null;
}
/**
 * Set standard rate limit response headers
 */
function setRateLimitHeaders(res, limit, remaining, resetAt) {
    res.setHeader('X-RateLimit-Limit', limit.toString());
    res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining).toString());
    res.setHeader('X-RateLimit-Reset', Math.ceil(resetAt / 1000).toString());
    res.setHeader('X-RateLimit-Reset-After', Math.ceil((resetAt - Date.now()) / 1000).toString());
}
/**
 * Apply rate limiting using sliding window algorithm
 */
function applyRateLimit(req, res, store, options) {
    const key = options.keyGenerator
        ? options.keyGenerator(req)
        : getRateLimitKey(req, options.keyPrefix);
    const now = Date.now();
    const windowStart = now - options.windowMs;
    let entry = store.get(key);
    if (!entry) {
        entry = {
            count: 0,
            resetAt: now + options.windowMs,
            requests: [],
        };
        store.set(key, entry);
    }
    // Sliding window: remove requests outside the window
    entry.requests = entry.requests.filter((t) => t > windowStart);
    entry.count = entry.requests.length;
    // Update reset time if window has passed
    if (entry.resetAt < now) {
        entry.resetAt = now + options.windowMs;
    }
    const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
    // Check if limit exceeded
    if (entry.count >= options.maxRequests) {
        return { allowed: false, entry, retryAfter };
    }
    // Add current request
    entry.requests.push(now);
    entry.count++;
    return { allowed: true, entry, retryAfter };
}
/**
 * Create rate limiting middleware with route-based configuration
 */
export function createRateLimitMiddleware(config) {
    // Merge default route limits with any custom configuration
    const routeLimits = {
        ...DEFAULT_ROUTE_LIMITS,
        ...(config.routeRateLimits || {}),
    };
    return (req, res, next) => {
        // Skip if rate limiting disabled
        if (!config.rateLimit) {
            return next();
        }
        // Find route-specific configuration
        const routeConfig = findRouteConfig(req.path, routeLimits);
        // Use route-specific limits or fall back to global config
        const options = routeConfig || {
            maxRequests: config.rateLimitMax,
            windowMs: config.rateLimitWindow,
            keyPrefix: 'global',
        };
        // Check if should skip
        if (options.skip && options.skip(req)) {
            return next();
        }
        const { allowed, entry, retryAfter } = applyRateLimit(req, res, rateLimitStore, options);
        // Set headers
        setRateLimitHeaders(res, options.maxRequests, options.maxRequests - entry.count, entry.resetAt);
        if (!allowed) {
            res.setHeader('Retry-After', retryAfter.toString());
            // Use custom handler if provided
            if (options.handler) {
                return options.handler(req, res, next, retryAfter);
            }
            return res.status(429).json({
                ...API_ERRORS.RATE_LIMITED,
                details: {
                    limit: options.maxRequests,
                    windowMs: options.windowMs,
                    retryAfter,
                    route: routeConfig ? req.path : 'global',
                },
            });
        }
        return next();
    };
}
/**
 * Create endpoint-specific rate limit middleware
 */
export function endpointRateLimit(maxRequests, windowMs) {
    const store = new Map();
    return (req, res, next) => {
        const options = {
            maxRequests,
            windowMs,
            keyPrefix: `endpoint:${req.path}`,
        };
        const { allowed, entry, retryAfter } = applyRateLimit(req, res, store, options);
        // Set headers
        setRateLimitHeaders(res, maxRequests, maxRequests - entry.count, entry.resetAt);
        if (!allowed) {
            res.setHeader('Retry-After', retryAfter.toString());
            return res.status(429).json({
                ...API_ERRORS.RATE_LIMITED,
                message: `Endpoint rate limit exceeded. Try again in ${retryAfter}s`,
                details: {
                    endpoint: req.path,
                    limit: maxRequests,
                    windowMs,
                    retryAfter,
                },
            });
        }
        return next();
    };
}
/**
 * Create a rate limiter for a specific route prefix
 * Useful for applying different limits to different route groups
 *
 * @example
 * // 10 requests per minute for auth routes
 * app.use('/auth', createRouteRateLimiter({ maxRequests: 10, windowMs: 60000 }));
 *
 * // 100 requests per minute for API routes
 * app.use('/api', createRouteRateLimiter({ maxRequests: 100, windowMs: 60000 }));
 */
export function createRouteRateLimiter(options) {
    const store = new Map();
    return (req, res, next) => {
        // Check if should skip
        if (options.skip && options.skip(req)) {
            return next();
        }
        const { allowed, entry, retryAfter } = applyRateLimit(req, res, store, options);
        // Set headers
        setRateLimitHeaders(res, options.maxRequests, options.maxRequests - entry.count, entry.resetAt);
        if (!allowed) {
            res.setHeader('Retry-After', retryAfter.toString());
            // Use custom handler if provided
            if (options.handler) {
                return options.handler(req, res, next, retryAfter);
            }
            return res.status(429).json({
                ...API_ERRORS.RATE_LIMITED,
                message: `Rate limit exceeded. Try again in ${retryAfter}s`,
                details: {
                    limit: options.maxRequests,
                    windowMs: options.windowMs,
                    retryAfter,
                },
            });
        }
        return next();
    };
}
/**
 * Predefined rate limiters for common use cases
 */
export const rateLimiters = {
    /** Strict limiter for auth endpoints: 10 req/min */
    auth: createRouteRateLimiter({
        maxRequests: 10,
        windowMs: 60000,
        keyPrefix: 'auth',
    }),
    /** Standard API limiter: 100 req/min */
    api: createRouteRateLimiter({
        maxRequests: 100,
        windowMs: 60000,
        keyPrefix: 'api',
    }),
    /** Relaxed limiter for read-only endpoints: 200 req/min */
    readonly: createRouteRateLimiter({
        maxRequests: 200,
        windowMs: 60000,
        keyPrefix: 'readonly',
    }),
    /** Very strict limiter for sensitive operations: 5 req/min */
    sensitive: createRouteRateLimiter({
        maxRequests: 5,
        windowMs: 60000,
        keyPrefix: 'sensitive',
    }),
};
/**
 * Get current rate limit stats for a key
 */
export function getRateLimitStats(keyId) {
    const entry = rateLimitStore.get(`key:${keyId}`);
    if (!entry) {
        return null;
    }
    const now = Date.now();
    const windowStart = now - 60000; // Assume 1 minute window
    const validRequests = entry.requests.filter((t) => t > windowStart);
    return {
        used: validRequests.length,
        remaining: Math.max(0, 60 - validRequests.length), // Assume 60 req/min
        resetAt: entry.resetAt,
    };
}
/**
 * Reset rate limit for a key (admin function)
 */
export function resetRateLimit(keyId) {
    return rateLimitStore.delete(`key:${keyId}`);
}
/**
 * Get all rate limit entries (admin function)
 */
export function getAllRateLimits() {
    return new Map(rateLimitStore);
}
//# sourceMappingURL=rate-limit.js.map