Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / quality (push) Successful in 41s
CI (SIAX Cloud) / security (pull_request) Successful in 22s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / quality (pull_request) Successful in 41s
BACKLOG P0-3: /api/health only ever checked *presence* of DATABASE_URL/
REDIS_URL and always returned ok:true regardless of whether anything was
reachable -- ARCHITECTURE.md's own words, "an uptime monitor watching HTTP
200 would show green for a dying instance".
lib/health-checks.ts adds real dependency probes:
- db: a live `select 1` round trip (not just DATABASE_URL presence)
- engine: a real HTTP call to the FastAPI engine's own GET /health
- stripe: Stripe's documented lightweight balance.retrieve() liveness call
- queue: honestly reports "not_integrated" when REDIS_URL is set but
unused -- no Redis client is wired anywhere in this repo, so reporting
"ok" would fabricate a check that doesn't exist
/api/health stays a liveness check (always 200 while the process can
respond; the body's `ok` field is the honest signal). New GET /api/ready
fails closed with 503 when the load-bearing dependency (db) isn't both
configured and reachable -- the "no /ready route" gap.
contracts/openapi.yaml, docs/architecture/ARCHITECTURE.md, and docs/slo.md
updated to match. 17 new unit tests (dependency-injected fakes, no real
network/DB) + 5 route-level tests.
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
/**
|
|
* `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\/ready(\/|$)/,
|
|
/^\/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();
|
|
}
|