/** * Next.js auth seam for the complete /api/0pt capability surface. * * Customer calls use verified Zitadel bearer identity and receive immutable * x-0pt-verified-* headers. Explicit machine routes may instead use the * shared X-Internal-Token convention; mutation handlers verify it again for * defence in depth. */ import { NextResponse, type NextRequest } from 'next/server'; import { assertAuthConfigSafe, resolveAuthContext } from '@/src/auth/context'; import { verifyInternalServiceToken } from '@/src/auth/internal-token'; assertAuthConfigSafe(); export const config = { matcher: ['/api/0pt', '/api/0pt/:path*'], }; const EXEMPT_PATTERNS: RegExp[] = [ /^\/api\/health(\/|$)/, /^\/api\/ready(\/|$)/, /^\/api\/admin\/metrics(\/|$)/, ]; /** Routes that may be reached with a service token instead of end-user bearer auth. */ const INTERNAL_SERVICE_ROUTES: RegExp[] = [ /^\/api\/0pt\/quality\/bridge-run$/, /^\/api\/0pt\/internal\/evidence-outbox\/dispatch$/, /^\/api\/0pt\/internal\/er\/candidates$/, /^\/api\/0pt\/search\/definitions$/, ]; let _authDiagCount = 0; export async function middleware(req: NextRequest): Promise { const path = req.nextUrl.pathname; if (EXEMPT_PATTERNS.some((re) => re.test(path))) return NextResponse.next(); if (INTERNAL_SERVICE_ROUTES.some((re) => re.test(path)) && verifyInternalServiceToken(req.headers)) { return NextResponse.next(); } if (_authDiagCount < 5) { _authDiagCount += 1; const hasAuth = !!req.headers.get('authorization'); console.log( `[0pt-auth] middleware path=${path} auth_header=${hasAuth} ` + `auth_enforce=${process.env.AUTH_ENFORCE ?? 'unset'} ` + `node_env=${process.env.NODE_ENV ?? 'unset'} request#${_authDiagCount}`, ); } 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(); }