@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
620 lines (619 loc) • 19.9 kB
JavaScript
/**
* Request Interceptors and Retry Logic
*
* Provides middleware interceptors for the NeuroLink client SDK including
* authentication, logging, retry logic, rate limiting, and request/response transformation.
*
* @module @neurolink/client/interceptors
*/
import { logger } from "../utils/logger.js";
// =============================================================================
// Utility Functions
// =============================================================================
/**
* Calculate delay for exponential backoff with jitter
*/
function calculateBackoffDelay(attempt, initialDelayMs, maxDelayMs, multiplier) {
const delay = initialDelayMs * Math.pow(multiplier, attempt);
const jitter = Math.random() * 0.1 * delay;
return Math.min(delay + jitter, maxDelayMs);
}
/**
* Delay utility
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Check if a status code is retryable
*/
function isRetryableStatus(status, retryableStatusCodes) {
return retryableStatusCodes.includes(status);
}
// =============================================================================
// Authentication Interceptors
// =============================================================================
/**
* API Key authentication interceptor
*
* Adds X-API-Key header to all requests.
*
* @example
* ```typescript
* client.use(createApiKeyAuthInterceptor('your-api-key'));
* ```
*/
export function createApiKeyAuthInterceptor(apiKey) {
return async (request, next) => {
request.headers["X-API-Key"] = apiKey;
return next();
};
}
/**
* Bearer token authentication interceptor
*
* Adds Authorization header with Bearer token.
*
* @example
* ```typescript
* client.use(createBearerAuthInterceptor('your-token'));
* ```
*/
export function createBearerAuthInterceptor(token) {
return async (request, next) => {
request.headers["Authorization"] = `Bearer ${token}`;
return next();
};
}
/**
* Dynamic authentication interceptor
*
* Retrieves authentication token dynamically for each request.
* Useful for token refresh scenarios.
*
* @example
* ```typescript
* client.use(createDynamicAuthInterceptor(async () => {
* const token = await getAccessToken();
* return { type: 'bearer', token };
* }));
* ```
*/
export function createDynamicAuthInterceptor(getAuth) {
return async (request, next) => {
const auth = await getAuth();
if (auth) {
if (auth.type === "apiKey") {
request.headers["X-API-Key"] = auth.key;
}
else if (auth.type === "bearer") {
request.headers["Authorization"] = `Bearer ${auth.token}`;
}
}
return next();
};
}
// =============================================================================
// Logging Interceptors
// =============================================================================
/**
* Logging interceptor
*
* Logs request and response details for debugging.
*
* @example
* ```typescript
* client.use(createLoggingInterceptor({
* logRequest: true,
* logResponse: true,
* redactFields: ['apiKey', 'password'],
* }));
* ```
*/
export function createLoggingInterceptor(options = {}) {
const { logRequest = true, logResponse = true, logBody = false, logResponseBody = false, logger: customLogger = (message, data) => logger.debug(message, data), redactFields = ["apiKey", "password", "token", "authorization"], } = options;
const redact = (obj) => {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(redact);
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (redactFields.some((field) => key.toLowerCase().includes(field.toLowerCase()))) {
result[key] = "[REDACTED]";
}
else if (typeof value === "object" && value !== null) {
result[key] = redact(value);
}
else {
result[key] = value;
}
}
return result;
};
return async (request, next) => {
const startTime = Date.now();
if (logRequest) {
const logData = {
method: request.method,
url: request.url,
requestId: request.context.requestId,
};
if (logBody && request.body) {
logData.body = redact(request.body);
}
customLogger(`[NeuroLink] Request`, logData);
}
try {
const response = await next();
const duration = Date.now() - startTime;
if (logResponse) {
const logData = {
status: response.status,
duration: `${duration}ms`,
requestId: request.context.requestId,
};
if (logResponseBody && response.body) {
logData.body = redact(response.body);
}
customLogger(`[NeuroLink] Response`, logData);
}
return response;
}
catch (error) {
const duration = Date.now() - startTime;
customLogger(`[NeuroLink] Error`, {
error: error.message,
duration: `${duration}ms`,
requestId: request.context.requestId,
});
throw error;
}
};
}
// =============================================================================
// Retry Interceptors
// =============================================================================
/**
* Retry interceptor with exponential backoff
*
* Automatically retries failed requests with configurable backoff.
*
* @example
* ```typescript
* client.use(createRetryInterceptor({
* maxAttempts: 3,
* initialDelayMs: 1000,
* maxDelayMs: 10000,
* backoffMultiplier: 2,
* retryableStatusCodes: [429, 500, 502, 503, 504],
* onRetry: (attempt, error) => console.log(`Retry ${attempt}:`, error),
* }));
* ```
*/
export function createRetryInterceptor(options) {
const { maxAttempts = 3, initialDelayMs = 1000, maxDelayMs = 10000, backoffMultiplier = 2, retryableStatusCodes = [408, 429, 500, 502, 503, 504], retryOnNetworkError = true, onRetry, shouldRetry, } = options;
return async (request, next) => {
let lastError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await next();
// Check custom retry condition
if (shouldRetry && shouldRetry(response, attempt)) {
if (attempt < maxAttempts - 1) {
const delay = calculateBackoffDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier);
onRetry?.(attempt + 1, { message: "Custom retry condition met" }, request);
await sleep(delay);
request.context.retryCount = attempt + 1;
continue;
}
}
// Check if response status is retryable
if (isRetryableStatus(response.status, retryableStatusCodes)) {
if (attempt < maxAttempts - 1) {
const delay = calculateBackoffDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier);
const error = {
code: "RETRYABLE_ERROR",
message: `HTTP ${response.status}`,
status: response.status,
retryable: true,
};
onRetry?.(attempt + 1, error, request);
await sleep(delay);
request.context.retryCount = attempt + 1;
continue;
}
}
return response;
}
catch (error) {
lastError = error;
// Check if network error and should retry
if (retryOnNetworkError && attempt < maxAttempts - 1) {
const delay = calculateBackoffDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier);
onRetry?.(attempt + 1, error, request);
await sleep(delay);
request.context.retryCount = attempt + 1;
continue;
}
throw error;
}
}
throw lastError ?? new Error("Max retries exceeded");
};
}
// =============================================================================
// Rate Limiting Interceptors
// =============================================================================
/**
* Token bucket rate limiter
*/
class TokenBucket {
tokens;
lastRefill;
maxTokens;
refillRate;
queue = [];
constructor(maxTokens, windowMs) {
this.maxTokens = maxTokens;
this.tokens = maxTokens;
this.lastRefill = Date.now();
this.refillRate = maxTokens / windowMs;
}
refill() {
const now = Date.now();
const elapsed = now - this.lastRefill;
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
async acquire(strategy) {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return;
}
if (strategy === "throw") {
throw new Error("Rate limit exceeded");
}
// Queue strategy: wait for a token
return new Promise((resolve, reject) => {
this.queue.push({ resolve, reject });
this.processQueue();
});
}
processQueue() {
if (this.queue.length === 0) {
return;
}
const waitTime = Math.ceil((1 - this.tokens) / this.refillRate);
setTimeout(() => {
this.refill();
while (this.tokens >= 1 && this.queue.length > 0) {
this.tokens -= 1;
const item = this.queue.shift();
item?.resolve();
}
if (this.queue.length > 0) {
this.processQueue();
}
}, waitTime);
}
}
/**
* Rate limiting interceptor
*
* Limits the rate of requests to prevent overwhelming the API.
*
* @example
* ```typescript
* client.use(createRateLimitInterceptor({
* maxRequests: 100,
* windowMs: 60000, // 100 requests per minute
* strategy: 'queue',
* onRateLimited: (waitTime) => console.log(`Rate limited, waiting ${waitTime}ms`),
* }));
* ```
*/
export function createRateLimitInterceptor(options) {
const { maxRequests, windowMs, strategy = "queue", onRateLimited } = options;
const bucket = new TokenBucket(maxRequests, windowMs);
return async (request, next) => {
try {
await bucket.acquire(strategy);
}
catch (error) {
onRateLimited?.(windowMs / maxRequests);
throw error;
}
return next();
};
}
// =============================================================================
// Request/Response Transformation Interceptors
// =============================================================================
/**
* Request transformation interceptor
*
* Transform request before sending.
*
* @example
* ```typescript
* client.use(createRequestTransformInterceptor((request) => {
* // Add custom header based on request body
* if (request.body?.priority === 'high') {
* request.headers['X-Priority'] = 'high';
* }
* return request;
* }));
* ```
*/
export function createRequestTransformInterceptor(transform) {
return async (request, next) => {
const transformedRequest = await transform(request);
// Update the original request object
Object.assign(request, transformedRequest);
return next();
};
}
/**
* Response transformation interceptor
*
* Transform response before returning.
*
* @example
* ```typescript
* client.use(createResponseTransformInterceptor((response) => {
* // Add metadata to response
* response.context.processedAt = Date.now();
* return response;
* }));
* ```
*/
export function createResponseTransformInterceptor(transform) {
return async (request, next) => {
const response = await next();
return transform(response);
};
}
// =============================================================================
// Caching Interceptors
// =============================================================================
/**
* Simple in-memory cache
*/
class SimpleCache {
cache = new Map();
maxSize;
constructor(maxSize = 1000) {
this.maxSize = maxSize;
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return undefined;
}
if (Date.now() > entry.expires) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}
set(key, value, ttl) {
// Evict oldest entries if cache is full
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, {
value,
expires: Date.now() + ttl,
});
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
/**
* Caching interceptor
*
* Caches responses to reduce API calls.
*
* @example
* ```typescript
* client.use(createCacheInterceptor({
* ttl: 60000, // 1 minute
* methods: ['GET'],
* includePaths: [/\/api\/tools/, /\/api\/providers/],
* }));
* ```
*/
export function createCacheInterceptor(options) {
const { ttl, maxSize = 1000, keyGenerator = (req) => `${req.method}:${req.url}`, methods = ["GET"], includePaths, excludePaths, } = options;
const cache = new SimpleCache(maxSize);
return async (request, next) => {
// Check if request should be cached
if (!methods.includes(request.method)) {
return next();
}
// Check path filters
const url = new URL(request.url, "http://localhost");
if (excludePaths?.some((pattern) => pattern.test(url.pathname))) {
return next();
}
if (includePaths &&
!includePaths.some((pattern) => pattern.test(url.pathname))) {
return next();
}
const cacheKey = keyGenerator(request);
const cached = cache.get(cacheKey);
if (cached) {
// Return cached response with cache hit indicator
return {
...cached,
context: {
...cached.context,
cacheHit: true,
},
};
}
const response = await next();
// Only cache successful responses
if (response.status >= 200 && response.status < 300) {
cache.set(cacheKey, response, ttl);
}
return response;
};
}
// =============================================================================
// Timeout Interceptors
// =============================================================================
/**
* Timeout interceptor
*
* Adds a timeout to requests.
*
* @example
* ```typescript
* client.use(createTimeoutInterceptor({
* timeout: 30000, // 30 seconds
* onTimeout: (request) => console.log('Request timed out:', request.url),
* }));
* ```
*/
export function createTimeoutInterceptor(options) {
const { timeout, onTimeout } = options;
return async (request, next) => {
const controller = new AbortController();
// Store the signal so downstream middleware/httpClient can also use it
request.context.timeoutSignal = controller.signal;
// Race next() against a timer that rejects on timeout
let timeoutId;
const timeoutPromise = new Promise((_resolve, reject) => {
timeoutId = setTimeout(() => {
controller.abort();
onTimeout?.(request);
const error = new Error(`Request timed out after ${timeout}ms`);
error.name = "TimeoutError";
reject(error);
}, timeout);
});
try {
const response = await Promise.race([next(), timeoutPromise]);
clearTimeout(timeoutId);
return response;
}
catch (error) {
clearTimeout(timeoutId);
if (error.name === "TimeoutError" ||
error.name === "AbortError") {
throw new Error(`Request timed out after ${timeout}ms`, {
cause: error,
});
}
throw error;
}
};
}
// =============================================================================
// Error Handling Interceptors
// =============================================================================
/**
* Error handling interceptor
*
* Provides centralized error handling and transformation.
*
* @example
* ```typescript
* client.use(createErrorHandlerInterceptor({
* onError: (error, request) => {
* console.error('Request failed:', error.message);
* },
* reportError: async (error, context) => {
* await errorReportingService.report(error, context);
* },
* }));
* ```
*/
export function createErrorHandlerInterceptor(options = {}) {
const { onError, transformError, reportError } = options;
return async (request, next) => {
try {
return await next();
}
catch (error) {
// Report error if configured
if (reportError) {
await reportError(error, request.context);
}
// Transform error if configured
if (transformError) {
const transformed = transformError(error);
throw transformed;
}
// Call custom handler
if (onError) {
const result = onError(error, request);
if (result) {
throw result;
}
}
throw error;
}
};
}
// =============================================================================
// Composition Utilities
// =============================================================================
/**
* Compose multiple middleware into one
*
* @example
* ```typescript
* const combinedMiddleware = composeMiddleware(
* createLoggingInterceptor(),
* createRetryInterceptor({ maxAttempts: 3 }),
* createRateLimitInterceptor({ maxRequests: 100, windowMs: 60000 }),
* );
*
* client.use(combinedMiddleware);
* ```
*/
export function composeMiddleware(...middlewares) {
return async (request, next) => {
let index = 0;
const executeNext = async () => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
return middleware(request, executeNext);
}
return next();
};
return executeNext();
};
}
/**
* Conditionally apply middleware
*
* @example
* ```typescript
* client.use(conditionalMiddleware(
* (request) => request.url.includes('/api/agents'),
* createLoggingInterceptor(),
* ));
* ```
*/
export function conditionalMiddleware(condition, middleware) {
return async (request, next) => {
if (condition(request)) {
return middleware(request, next);
}
return next();
};
}