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

297 lines 11.6 kB
/** * Common Middleware Components * Utility middleware for common server operations */ import { getMetricsAggregator, SpanSerializer, SpanStatus, SpanType, } from "../../observability/index.js"; import { SpanStatusCode } from "@opentelemetry/api"; import { tracers } from "../../telemetry/tracers.js"; import { logger } from "../../utils/logger.js"; /** * Create request timing middleware * Adds timing information to responses * * @example * ```typescript * server.registerMiddleware(createTimingMiddleware()); * ``` */ export function createTimingMiddleware() { return { name: "timing", order: 0, // Run first handler: async (ctx, next) => { return tracers.middleware.startActiveSpan("neurolink.middleware.timing", async (otelSpan) => { try { const startTime = Date.now(); const startHrTime = process.hrtime.bigint(); // Store start time in metadata ctx.metadata.requestStartTime = startTime; const span = SpanSerializer.createSpan(SpanType.SERVER_REQUEST, "server.middleware.timing", { "server.operation": "middleware", "server.middleware": "timing", }); try { const result = await next(); // Calculate duration const endHrTime = process.hrtime.bigint(); const durationNs = Number(endHrTime - startHrTime); const durationMs = durationNs / 1_000_000; // Add timing headers to responseHeaders (adapters read from here) ctx.responseHeaders = ctx.responseHeaders || {}; ctx.responseHeaders["X-Response-Time"] = `${durationMs.toFixed(2)}ms`; ctx.responseHeaders["Server-Timing"] = `total;dur=${durationMs.toFixed(2)}`; span.durationMs = Date.now() - startTime; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK); getMetricsAggregator().recordSpan(endedSpan); return result; } catch (error) { // Propagate error to OTel span so traces show ERROR status otelSpan.recordException(error instanceof Error ? error : new Error(String(error))); otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error), }); span.durationMs = Date.now() - startTime; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.ERROR); endedSpan.statusMessage = error instanceof Error ? error.message : String(error); getMetricsAggregator().recordSpan(endedSpan); throw error; } } finally { otelSpan.end(); } }); // end startActiveSpan }, }; } /** * Create request ID middleware * Ensures every request has a unique ID * * @example * ```typescript * server.registerMiddleware(createRequestIdMiddleware()); * ``` */ export function createRequestIdMiddleware(options) { const { headerName = "x-request-id", prefix = "req", generator = () => `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, } = options ?? {}; return { name: "request-id", order: 0, // Run first handler: async (ctx, next) => { // Use existing ID or generate new one ctx.requestId = ctx.headers[headerName.toLowerCase()] || ctx.requestId || generator(); // Add request ID header to responseHeaders (adapters read from here) ctx.responseHeaders = ctx.responseHeaders || {}; ctx.responseHeaders["X-Request-ID"] = ctx.requestId; return next(); }, }; } /** * Create error handling middleware * Catches errors and formats them consistently * * @example * ```typescript * server.registerMiddleware(createErrorHandlingMiddleware({ * includeStack: process.env.NODE_ENV === 'development', * })); * ``` */ export function createErrorHandlingMiddleware(options) { const { includeStack = false, onError, logErrors = true } = options ?? {}; return { name: "error-handling", order: 1, // Run very early to catch all errors handler: async (ctx, next) => { const span = SpanSerializer.createSpan(SpanType.SERVER_REQUEST, "server.middleware.errorHandling", { "server.operation": "middleware", "server.middleware": "error-handling", }); const middlewareStartTime = Date.now(); try { const result = await next(); span.durationMs = Date.now() - middlewareStartTime; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK); getMetricsAggregator().recordSpan(endedSpan); return result; } catch (error) { const err = error; if (logErrors) { logger.error("[ErrorMiddleware]", { requestId: ctx.requestId, error: err.message, stack: err.stack, }); } span.durationMs = Date.now() - middlewareStartTime; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.ERROR); endedSpan.statusMessage = err.message; getMetricsAggregator().recordSpan(endedSpan); // Use custom handler if provided if (onError) { return onError(err, ctx); } // Default error response const statusCode = err.statusCode || 500; const errorResponse = { error: { code: err.code || `HTTP_${statusCode}`, message: statusCode === 500 ? "Internal server error" : err.message, }, metadata: { requestId: ctx.requestId, timestamp: new Date().toISOString(), }, }; // Include stack in development if (includeStack && err.stack) { errorResponse.error.stack = err.stack; } // Store error response in metadata ctx.metadata.errorResponse = errorResponse; ctx.metadata.errorStatusCode = statusCode; // Re-throw for adapter to handle throw error; } }, }; } /** * Create security headers middleware * Adds common security headers to responses * * @example * ```typescript * server.registerMiddleware(createSecurityHeadersMiddleware()); * ``` */ export function createSecurityHeadersMiddleware(options) { const { contentSecurityPolicy, frameOptions = "DENY", contentTypeOptions = "nosniff", hstsMaxAge = 31536000, referrerPolicy = "strict-origin-when-cross-origin", customHeaders = {}, } = options ?? {}; // Build headers object const securityHeaders = {}; if (contentSecurityPolicy) { securityHeaders["Content-Security-Policy"] = contentSecurityPolicy; } if (frameOptions) { securityHeaders["X-Frame-Options"] = frameOptions; } if (contentTypeOptions) { securityHeaders["X-Content-Type-Options"] = contentTypeOptions; } if (hstsMaxAge !== false) { securityHeaders["Strict-Transport-Security"] = `max-age=${hstsMaxAge}; includeSubDomains`; } if (referrerPolicy) { securityHeaders["Referrer-Policy"] = referrerPolicy; } // Add X-XSS-Protection for older browsers securityHeaders["X-XSS-Protection"] = "1; mode=block"; // Add custom headers Object.assign(securityHeaders, customHeaders); return { name: "security-headers", order: 2, handler: async (ctx, next) => { // Add security headers to responseHeaders (adapters read from here) ctx.responseHeaders = ctx.responseHeaders || {}; Object.assign(ctx.responseHeaders, securityHeaders); return next(); }, }; } /** * Create request logging middleware * Logs request and response information * * @example * ```typescript * server.registerMiddleware(createLoggingMiddleware({ * logBody: process.env.NODE_ENV === 'development', * })); * ``` */ export function createLoggingMiddleware(options) { const { logBody = false, logResponse = false, logger = console, skipPaths = ["/health", "/ready", "/metrics"], } = options ?? {}; return { name: "logging", order: 3, excludePaths: skipPaths, handler: async (ctx, next) => { const startTime = Date.now(); // Log request const requestLog = { requestId: ctx.requestId, method: ctx.method, path: ctx.path, query: Object.keys(ctx.query).length > 0 ? ctx.query : undefined, }; if (logBody && ctx.body) { requestLog.body = ctx.body; } logger.info(`[Request] ${ctx.method} ${ctx.path}`, requestLog); try { const result = await next(); const duration = Date.now() - startTime; // Log response const responseLog = { requestId: ctx.requestId, duration: `${duration}ms`, status: 200, }; if (logResponse && result) { responseLog.response = result; } logger.info(`[Response] ${ctx.method} ${ctx.path}`, responseLog); return result; } catch (error) { const duration = Date.now() - startTime; const err = error; logger.error(`[Error] ${ctx.method} ${ctx.path}`, { requestId: ctx.requestId, duration: `${duration}ms`, error: err.message, status: err.statusCode || 500, }); throw error; } }, }; } /** * Create compression preference middleware * Signals compression preference to adapters */ export function createCompressionMiddleware(options) { const { threshold = 1024, contentTypes = [ "text/", "application/json", "application/javascript", "application/xml", ], } = options ?? {}; return { name: "compression", order: 5, handler: async (ctx, next) => { // Store compression preferences in metadata ctx.metadata.compression = { enabled: true, threshold, contentTypes, acceptEncoding: ctx.headers["accept-encoding"] || "", }; return next(); }, }; } //# sourceMappingURL=common.js.map