/** * CORS Middleware * Agent 4: Production Deployment * * Configures Cross-Origin Resource Sharing for API routes. */ import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next'; import { env } from '../config'; interface CorsConfig { origins: string[]; methods: string[]; allowedHeaders: string[]; exposedHeaders: string[]; credentials: boolean; maxAge: number; } const DEFAULT_CONFIG: CorsConfig = { origins: env.corsOrigins, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Requested-With', 'X-Request-Id', 'X-API-Key', ], exposedHeaders: [ 'X-Request-Id', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset', ], credentials: true, maxAge: 86400, // 24 hours }; /** * Check if origin is allowed */ function isOriginAllowed(origin: string | undefined, allowedOrigins: string[]): boolean { if (!origin) return false; return allowedOrigins.some((allowed) => { // Exact match if (allowed === origin) return true; // Wildcard subdomain match (e.g., *.example.com) if (allowed.startsWith('*.')) { const domain = allowed.slice(2); return origin.endsWith(domain) || origin === `https://${domain}` || origin === `http://${domain}`; } // All origins allowed if (allowed === '*') return true; return false; }); } /** * Apply CORS headers to response */ export function applyCorsHeaders( req: NextApiRequest, res: NextApiResponse, config: CorsConfig = DEFAULT_CONFIG ): void { const origin = req.headers.origin; // Set allowed origin if (isOriginAllowed(origin, config.origins)) { res.setHeader('Access-Control-Allow-Origin', origin!); } else if (config.origins.includes('*')) { res.setHeader('Access-Control-Allow-Origin', '*'); } // Set other CORS headers if (config.credentials) { res.setHeader('Access-Control-Allow-Credentials', 'true'); } res.setHeader('Access-Control-Allow-Methods', config.methods.join(', ')); res.setHeader('Access-Control-Allow-Headers', config.allowedHeaders.join(', ')); res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', ')); res.setHeader('Access-Control-Max-Age', config.maxAge.toString()); } /** * CORS middleware for API routes */ export function withCors( handler: NextApiHandler, config?: Partial ): NextApiHandler { const mergedConfig: CorsConfig = { ...DEFAULT_CONFIG, ...config }; return async (req: NextApiRequest, res: NextApiResponse) => { applyCorsHeaders(req, res, mergedConfig); // Handle preflight requests if (req.method === 'OPTIONS') { return res.status(204).end(); } return handler(req, res); }; } /** * Strict CORS for internal APIs only */ export const strictCors = (handler: NextApiHandler) => withCors(handler, { origins: ['http://localhost:3001'], credentials: true, }); /** * Open CORS for public APIs */ export const openCors = (handler: NextApiHandler) => withCors(handler, { origins: ['*'], credentials: false, }); // Export types export type { CorsConfig };