import { NextRequest, NextResponse } from 'next/server'; import { hasDeploymentCapability, resolveDeploymentRole } from '@/lib/config/deployment-role'; import { isApiPath, shouldReturnNotFoundAtServerBoundary, } from '@/lib/config/server-request-boundary'; import { verifyAccessTokenWithWebCrypto } from '@/lib/server/access-token-edge'; import { ACCESS_CODE_COOKIE_NAME } from '@/lib/server/access-token-policy'; export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const role = resolveDeploymentRole( process.env.OPENMAIC_DEPLOYMENT_ROLE ?? process.env.NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE, process.env.NODE_ENV, ); if (shouldReturnNotFoundAtServerBoundary(role, pathname, request.method)) { if (isApiPath(pathname)) { return NextResponse.json( { success: false, errorCode: 'NOT_FOUND', error: 'Not found' }, { status: 404 }, ); } return new NextResponse('Not Found', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8', 'x-content-type-options': 'nosniff', }, }); } const isOpsPath = pathname === '/courses' || pathname.startsWith('/courses/') || pathname === '/publish' || pathname === '/api/courses' || pathname.startsWith('/api/courses/') || pathname === '/api/ops' || pathname.startsWith('/api/ops/'); if (isOpsPath && !hasDeploymentCapability(role, 'manage_courses')) { if (pathname.startsWith('/api/')) { return NextResponse.json( { success: false, errorCode: 'FORBIDDEN', error: 'Operations capability is disabled', }, { status: 403 }, ); } return NextResponse.redirect(new URL('/', request.url)); } // Remaining APIs on a dedicated server deployment authenticate at their // route boundaries. A shared ACCESS_CODE from the ops environment must // never preempt the internal Bearer publishing contract. if (role === 'server') { return NextResponse.next(); } const accessCode = process.env.ACCESS_CODE; if (isOpsPath && process.env.NODE_ENV === 'production' && !accessCode) { return NextResponse.json( { success: false, errorCode: 'INTERNAL_ERROR', error: 'Operations deployment is not configured with ACCESS_CODE', }, { status: 503 }, ); } if (!accessCode) { return NextResponse.next(); } // Whitelist: access-code endpoints, health check if (pathname.startsWith('/api/access-code/') || pathname === '/api/health') { return NextResponse.next(); } // Check cookie — validate HMAC signature, not just existence const cookie = request.cookies.get(ACCESS_CODE_COOKIE_NAME); if (cookie?.value && (await verifyAccessTokenWithWebCrypto(cookie.value, accessCode))) { return NextResponse.next(); } // API requests without valid cookie → 401 if (pathname.startsWith('/api/')) { return NextResponse.json( { success: false, errorCode: 'INVALID_REQUEST', error: 'Access code required' }, { status: 401 }, ); } // Page requests → let through, frontend shows modal return NextResponse.next(); } export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|logos/).*)'], };