import { NextRequest, NextResponse } from "next/server"; /** * Next.js 16 Proxy (formerly Middleware) * * Generates a cryptographic nonce for each request to allow inline scripts * while maintaining Content Security Policy protection. * * @see https://nextjs.org/docs/app/guides/content-security-policy * @see https://nextjs.org/docs/messages/middleware-to-proxy */ export function proxy(request: NextRequest) { // Generate a random nonce for this request const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); // Determine environment const isDev = process.env.NODE_ENV === "development"; // Build CSP header value const cspHeader = buildCSP(nonce, isDev); // Clone the request headers const requestHeaders = new Headers(request.headers); requestHeaders.set("x-nonce", nonce); // Create response with updated headers const response = NextResponse.next({ request: { headers: requestHeaders, }, }); // Set CSP header on response response.headers.set("Content-Security-Policy", cspHeader); // Add additional security headers response.headers.set("X-Frame-Options", "DENY"); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); response.headers.set("X-XSS-Protection", "1; mode=block"); response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); return response; } function buildCSP(nonce: string, isDev: boolean): string { if (isDev) { // Development: More permissive for HMR and dev tools return [ "default-src 'self'", "script-src 'self' 'unsafe-eval' 'unsafe-inline'", // HMR needs eval "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self' https: http://localhost:* ws://localhost:*", "frame-ancestors 'none'", ].join("; "); } // Production: Strict CSP with nonce // 'strict-dynamic' allows scripts loaded by nonced scripts to execute. // Next 16 applies the nonce to its own inline scripts, so 'unsafe-inline' // is not required in script-src when the nonce is present. return [ "default-src 'self'", `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, "style-src 'self' 'unsafe-inline'", // Next.js requires this for styled-jsx "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self' https:", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", "object-src 'none'", "upgrade-insecure-requests", ].join("; "); } // Apply proxy to all routes except static assets export const config = { matcher: [ /* * Match all request paths except: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, sitemap.xml, robots.txt (metadata files) * - api/health (health check endpoint) */ { source: "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)", missing: [ { type: "header", key: "next-router-prefetch" }, { type: "header", key: "purpose", value: "prefetch" }, ], }, ], };