Files
0pt/middleware.ts
T

71 lines
2.3 KiB
TypeScript

/**
* 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<NextResponse> {
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();
}