diff --git a/middleware.ts b/middleware.ts index 1af30ef..cfd89f4 100644 --- a/middleware.ts +++ b/middleware.ts @@ -35,13 +35,31 @@ const INTERNAL_SERVICE_ROUTES: RegExp[] = [ ]; let _authDiagCount = 0; +let internalTenantWarned = false; 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(); + // P014/P0 tenant isolation (2026-11-07): the internal-token path used to + // return next() WITHOUT a verified tenant, which made + // assertTenantMatchesVerified a no-op and let a service principal write + // ANY tenant_id (proven: POST canonical/facts with tenant_id + // 999999999999999999 returned 201). When INTERNAL_TENANT_ID is configured + // the guard is now enforced exactly like a verified Bearer principal. + const forwarded = new Headers(req.headers); + const internalTenant = + req.headers.get('x-0pt-internal-tenant') ?? process.env.INTERNAL_TENANT_ID; + if (internalTenant) { + forwarded.set('x-0pt-verified-tenant', internalTenant); + } else if (!internalTenantWarned) { + internalTenantWarned = true; + console.warn( + '[0pt-auth] INTERNAL_TENANT_ID is unset: internal-token requests are not tenant-scoped (set it in production)', + ); + } + return NextResponse.next({ request: { headers: forwarded } }); } if (_authDiagCount < 5) {