Files
0pt/middleware.ts
T
SIAX Foundation BotandClaude Opus 4.8 1d76d50f3d
CI (SIAX Cloud) / security (push) Failing after 28s
CI (SIAX Cloud) / quality (push) Failing after 44s
CI (SIAX Cloud) / security (pull_request) Failing after 26s
CI (SIAX Cloud) / quality (pull_request) Failing after 33s
feat(auth,foundation): Wave 2 flag-gated Zitadel JWT verify + validation + eslint + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:32:34 +02:00

56 lines
2.1 KiB
TypeScript

/**
* `middleware.ts` -- Next.js request middleware = 0pt's framework-level auth
* seam (the Next analog of a Fastify `onRequest` hook).
*
* Wave 2 hardening: this runs the flag-gated 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.
*
* AUTH_ENFORCE != "true" (default): never rejects; if a verified token is
* present the verified principal is forwarded to the handler as
* `x-0pt-verified-sub` / `x-0pt-verified-tenant` request headers.
* AUTH_ENFORCE == "true": a valid verified token is required; 401 on
* missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant.
*
* 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 { resolveAuthContext } from '@/src/auth/context';
// 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<NextResponse> {
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();
}