UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

228 lines 8.93 kB
/** * Rate Limiting Middleware * Provides configurable rate limiting for server adapters */ import { RateLimitError as ServerRateLimitError } from "../errors.js"; /** * In-memory rate limit store */ export class InMemoryRateLimitStore { store = new Map(); cleanupInterval; constructor() { // Clean up expired entries periodically this.cleanupInterval = setInterval(() => this.cleanup(), 60000); } async get(key) { const record = this.store.get(key); if (!record) { return undefined; } // Check if expired if (Date.now() > record.resetAt) { this.store.delete(key); return undefined; } return record; } async set(key, entry) { this.store.set(key, entry); } async increment(key, windowMs) { const now = Date.now(); let record = this.store.get(key); if (!record || now > record.resetAt) { record = { count: 0, resetAt: now + windowMs }; this.store.set(key, record); } record.count++; return { count: record.count, resetAt: record.resetAt }; } async reset(key) { this.store.delete(key); } cleanup() { const now = Date.now(); for (const [key, record] of this.store.entries()) { if (now > record.resetAt) { this.store.delete(key); } } } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.store.clear(); } } /** * Create rate limiting middleware * * Response headers set on all requests: * - `X-RateLimit-Limit`: Maximum requests allowed per window * - `X-RateLimit-Remaining`: Requests remaining in current window * - `X-RateLimit-Reset`: Unix timestamp when the window resets * * Additional headers on rate limit exceeded (HTTP 429): * - `Retry-After`: Seconds to wait before retrying * * @example * ```typescript * const rateLimiter = createRateLimitMiddleware({ * maxRequests: 100, * windowMs: 15 * 60 * 1000, // 15 minutes * skipPaths: ["/api/health"], * }); * * server.registerMiddleware(rateLimiter); * ``` */ export function createRateLimitMiddleware(config) { const { maxRequests, windowMs, message = "Too many requests, please try again later", skipPaths = [], keyGenerator = defaultKeyGenerator, onRateLimitExceeded, store = new InMemoryRateLimitStore(), } = config; return { name: "rate-limit", order: 5, // Run early excludePaths: skipPaths, handler: async (ctx, next) => { const key = keyGenerator(ctx); const { count, resetAt } = await store.increment(key, windowMs); // Set rate limit headers in responseHeaders (adapters read from here) ctx.responseHeaders = ctx.responseHeaders || {}; ctx.responseHeaders["X-RateLimit-Limit"] = String(maxRequests); ctx.responseHeaders["X-RateLimit-Remaining"] = String(Math.max(0, maxRequests - count)); ctx.responseHeaders["X-RateLimit-Reset"] = String(Math.ceil(resetAt / 1000)); if (count > maxRequests) { const retryAfter = Math.ceil((resetAt - Date.now()) / 1000); // Add Retry-After header for 429 responses ctx.responseHeaders["Retry-After"] = String(Math.max(0, retryAfter)); if (onRateLimitExceeded) { return onRateLimitExceeded(ctx, retryAfter); } // Throw a rate limit error that adapters can catch throw new ServerRateLimitError(retryAfter * 1000, message, ctx.requestId); } return next(); }, }; } /** * Default key generator using IP address */ function defaultKeyGenerator(ctx) { return (ctx.headers["x-forwarded-for"]?.split(",")[0].trim() || ctx.headers["x-real-ip"] || "unknown"); } /** * Re-export RateLimitError from errors for convenience */ export { RateLimitError } from "../errors.js"; /** * Create a sliding window rate limiter * More accurate than fixed window but slightly more complex */ export function createSlidingWindowRateLimitMiddleware(config) { const { maxRequests, windowMs, message = "Too many requests, please try again later", skipPaths = [], keyGenerator = defaultKeyGenerator, onRateLimitExceeded, subWindows = 10, } = config; // Store: key -> array of timestamps const store = new Map(); const subWindowMs = windowMs / subWindows; // Cleanup interval setInterval(() => { const now = Date.now(); for (const [key, timestamps] of store.entries()) { const valid = timestamps.filter((t) => now - t < windowMs); if (valid.length === 0) { store.delete(key); } else { store.set(key, valid); } } }, subWindowMs); return { name: "sliding-window-rate-limit", order: 5, excludePaths: skipPaths, handler: async (ctx, next) => { const key = keyGenerator(ctx); const now = Date.now(); // Get existing timestamps and filter old ones let timestamps = store.get(key) || []; timestamps = timestamps.filter((t) => now - t < windowMs); // Calculate count const count = timestamps.length; // Set rate limit headers in responseHeaders (adapters read from here) ctx.responseHeaders = ctx.responseHeaders || {}; ctx.responseHeaders["X-RateLimit-Limit"] = String(maxRequests); ctx.responseHeaders["X-RateLimit-Remaining"] = String(Math.max(0, maxRequests - count - 1)); ctx.responseHeaders["X-RateLimit-Reset"] = String(Math.ceil((now + windowMs) / 1000)); if (count >= maxRequests) { const oldestTimestamp = timestamps[0] || now; const retryAfter = Math.ceil((oldestTimestamp + windowMs - now) / 1000); // Add Retry-After header for 429 responses ctx.responseHeaders["Retry-After"] = String(Math.max(0, retryAfter)); if (onRateLimitExceeded) { return onRateLimitExceeded(ctx, retryAfter); } throw new ServerRateLimitError(retryAfter * 1000, message, ctx.requestId); } // Add current request timestamp timestamps.push(now); store.set(key, timestamps); return next(); }, }; } // ============================================ // Compatibility Aliases // ============================================ /** * Alias for InMemoryRateLimitStore for compatibility */ export { InMemoryRateLimitStore as MemoryRateLimitStore }; /** * Create fixed window rate limit middleware * * Accepts config and optional store as separate parameters for compatibility. * Returns rate limit headers in the response object. * * @example * ```typescript * const store = new MemoryRateLimitStore(); * const middleware = createFixedWindowRateLimitMiddleware( * { windowMs: 60000, maxRequests: 10 }, * store * ); * ``` */ export function createFixedWindowRateLimitMiddleware(config, store) { const { maxRequests, windowMs, message = "Too many requests, please try again later", skipPaths = [], keyGenerator = defaultKeyGenerator, onRateLimitExceeded, } = config; const rateLimitStore = store ?? new InMemoryRateLimitStore(); return { name: "fixed-window-rate-limit", order: 5, excludePaths: skipPaths, handler: async (ctx, next) => { const key = keyGenerator(ctx); const { count, resetAt } = await rateLimitStore.increment(key, windowMs); // Set rate limit headers in responseHeaders (adapters read from here) ctx.responseHeaders = ctx.responseHeaders || {}; ctx.responseHeaders["X-RateLimit-Limit"] = String(maxRequests); ctx.responseHeaders["X-RateLimit-Remaining"] = String(Math.max(0, maxRequests - count)); ctx.responseHeaders["X-RateLimit-Reset"] = String(Math.ceil(resetAt / 1000)); if (count > maxRequests) { const retryAfter = Math.ceil((resetAt - Date.now()) / 1000); // Add Retry-After header for 429 responses ctx.responseHeaders["Retry-After"] = String(Math.max(0, retryAfter)); if (onRateLimitExceeded) { return onRateLimitExceeded(ctx, retryAfter); } throw new ServerRateLimitError(retryAfter * 1000, message, ctx.requestId); } // Call next handler - headers are already in ctx.responseHeaders for adapters return next(); }, }; } //# sourceMappingURL=rateLimit.js.map