/** * `middleware.ts` -- Next.js request middleware = 0pt's framework-level auth * seam (the Next analog of a Fastify `onRequest` hook). * * This runs the Zitadel verification (src/auth/context.ts -> * src/auth/zitadel.ts) in front of every 0pt capability route (/api/0pt/*). * It does NOT touch the v0-generated UI routes or the unrelated * payment/order APIs (see the matcher below and the note in the PR that * introduced the fail-closed default). * * AUTH_ENFORCE unset, or anything except "false" (DEFAULT, enforced): * a valid verified token is required; 401 on missing/invalid, 403 when * X-Tenant-Id disagrees with the token tenant. On success the verified * principal is forwarded to the handler as `x-0pt-verified-sub` / * `x-0pt-verified-tenant` request headers. * AUTH_ENFORCE == "false" (explicit local-dev escape hatch): never rejects. * REFUSED in production -- `assertAuthConfigSafe()` below throws at module * load, so a production instance cannot boot with auth switched off. * * Health/metrics are exempt (they are also outside the matcher below, but the * exemption is kept explicit for robustness). */ import { NextResponse, type NextRequest } from 'next/server'; import { assertAuthConfigSafe, resolveAuthContext } from '@/src/auth/context'; // STARTUP GATE: fail to boot rather than serve /api/0pt/* openly in production. assertAuthConfigSafe(); // Only guard the 0pt capability surface. The matcher keeps this middleware off // the UI, auth, and payment routes entirely. export const config = { matcher: ['/api/0pt/:path*'], }; const EXEMPT_PATTERNS: RegExp[] = [/^\/api\/health(\/|$)/, /^\/api\/admin\/metrics(\/|$)/]; export async function middleware(req: NextRequest): Promise { const path = req.nextUrl.pathname; if (EXEMPT_PATTERNS.some((re) => re.test(path))) { return NextResponse.next(); } const decision = await resolveAuthContext(req.headers); if (!decision.ok) { return NextResponse.json( { error: decision.code, message: decision.message, ...(decision.details ? { details: decision.details } : {}) }, { status: decision.status }, ); } const principal = decision.ctx.principal; if (principal) { const forwarded = new Headers(req.headers); forwarded.set('x-0pt-verified-sub', principal.sub); forwarded.set('x-0pt-verified-tenant', principal.tenantId); return NextResponse.next({ request: { headers: forwarded } }); } return NextResponse.next(); }