| // @polsia:shared β edit only through declared slots. Code installed by polsia/template-next@0.3.0. | |
| // | |
| // D18: Next.js 16 renamed `middleware.ts` β `proxy.ts`. Do NOT create a | |
| // `middleware.ts`. The Polsia validator rejects the old name (it is not a | |
| // local biome rule β `npm run lint` only scans src/tests). | |
| // | |
| // This file is the load-bearing wire-up for: | |
| // - CSP with per-request nonce | |
| // - frame-ancestors 'none' | |
| // - The `composed` middleware_chain slot | |
| // | |
| // Modules add middleware through the declared slot, NOT by editing this | |
| // file directly. The CSP is built in `src/lib/csp.ts`: script-src stays | |
| // strict (nonce + 'strict-dynamic', no 'unsafe-inline'/'unsafe-eval' in | |
| // production) β that is a permanent block. style-src deliberately allows | |
| // 'unsafe-inline' so headless UI primitives (Radix/shadcn) work in prod; the | |
| // rationale lives in csp.ts and the posture is locked by tests/unit/csp.test.ts. | |
| import { type NextRequest, NextResponse } from 'next/server'; | |
| import { buildCsp } from '@/lib/csp'; | |
| export function proxy(request: NextRequest) { | |
| const nonce = Buffer.from(crypto.randomUUID()).toString('base64'); | |
| const isDev = process.env.NODE_ENV === 'development'; | |
| // @polsia:slot middleware_chain start | |
| // Modules contribute middleware entries (e.g., rate-limit, webhook | |
| // signature pre-checks) here via their manifest `contributions` block. | |
| // The installer maintains ordering per the module's `ordering` field. | |
| // Do NOT hand-edit this slot β the install-hash check will reject it. | |
| // @polsia:slot middleware_chain end | |
| // CSP β script-src strict (nonce + 'strict-dynamic'); style-src relaxed for | |
| // headless UI. Built in src/lib/csp.ts (single source, unit-tested). The | |
| // per-request nonce is still exposed via x-nonce for script-src consumers. | |
| const csp = buildCsp(nonce, isDev, process.env.NEXT_PUBLIC_API_URL ?? ''); | |
| const requestHeaders = new Headers(request.headers); | |
| requestHeaders.set('x-nonce', nonce); | |
| requestHeaders.set('Content-Security-Policy', csp); | |
| const response = NextResponse.next({ request: { headers: requestHeaders } }); | |
| response.headers.set('Content-Security-Policy', csp); | |
| return response; | |
| } | |
| export const config = { | |
| matcher: [ | |
| { | |
| source: '/((?!api|_next/static|_next/image|favicon.ico).*)', | |
| missing: [ | |
| { type: 'header', key: 'next-router-prefetch' }, | |
| { type: 'header', key: 'purpose', value: 'prefetch' }, | |
| ], | |
| }, | |
| ], | |
| }; | |