@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 (227 loc) • 8.89 kB
JavaScript
/**
* 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();
},
};
}