diff --git a/server/src/bootstrap.ts b/server/src/bootstrap.ts index c2c7dda24..d270422fb 100644 --- a/server/src/bootstrap.ts +++ b/server/src/bootstrap.ts @@ -6,9 +6,10 @@ import { AppModule } from './nest/app.module'; import { apiDocsEnabled } from './nest/common/api-docs.kill-switch'; import { validateBodyContracts } from './nest/common/validate-body-contracts'; import { validateManagedRoutes } from './nest/common/validate-managed-routes'; -import { validateRouteGuards } from './nest/common/validate-route-guards'; +import { PUBLIC_ROUTE_ALLOW_LIST, validateRouteGuards } from './nest/common/validate-route-guards'; import { DatabaseLifecycle } from './nest/database/database-lifecycle.service'; import { withRequestContext } from './nest/database/request-context'; +import { MANAGED_PUBLIC_ROUTES } from './nest/managed/managed-ext.module'; import { setupApiDocs } from './nest/platform/api-docs'; import { MCP_METADATA_MIDDLEWARE } from './nest/platform/mcp-metadata.middleware'; import { applyPlatformUploads, applyPlatformStatic } from './nest/platform/platform.routes'; @@ -247,7 +248,8 @@ export async function buildApp(): Promise { // Fail closed on the anonymous surface too: a route that answers without a // session must carry @Public() with a reason AND be on the reviewed list. // Default-deny protects what exists; this is what keeps the exemptions honest. - validateRouteGuards(app); + // The managed attachment point declares its own anonymous routes; empty in this repository. + validateRouteGuards(app, [...PUBLIC_ROUTE_ALLOW_LIST, ...MANAGED_PUBLIC_ROUTES]); // And on the other direction of the same question: a route that a centrally // administered install withholds from its own admin must say why, and be on a // list somebody reviewed. Runs in every e2e harness because they share this diff --git a/server/src/nest/common/validate-route-guards.ts b/server/src/nest/common/validate-route-guards.ts index c8b01440b..5106a88d2 100644 --- a/server/src/nest/common/validate-route-guards.ts +++ b/server/src/nest/common/validate-route-guards.ts @@ -2,7 +2,6 @@ import { CookieAuthGuard } from '../auth-core/cookie-auth.guard'; import { JwtAuthGuard } from '../auth-core/jwt-auth.guard'; import { OptionalJwtGuard } from '../auth-core/optional-jwt.guard'; import { IS_PUBLIC, OPTIONAL_AUTH } from '../auth-core/public.decorator'; -import { MANAGED_PUBLIC_ROUTES } from '../managed/managed-ext.module'; import { ApiTokenGuard } from '../public-api/api-token.guard'; import type { INestApplication } from '@nestjs/common'; import { METHOD_METADATA, PATH_METADATA, GUARDS_METADATA } from '@nestjs/common/constants'; @@ -182,7 +181,7 @@ export const ANONYMOUS_GUARDED_ROUTE_ALLOW_LIST: string[] = [ */ export function validateRouteGuards( app: INestApplication, - allowList: string[] = [...PUBLIC_ROUTE_ALLOW_LIST, ...MANAGED_PUBLIC_ROUTES], + allowList: string[] = PUBLIC_ROUTE_ALLOW_LIST, anonymousGuardedAllowList: string[] = ANONYMOUS_GUARDED_ROUTE_ALLOW_LIST, ): void { const entries = collectRouteGuards(app); diff --git a/server/src/nest/managed/managed-ext.module.ts b/server/src/nest/managed/managed-ext.module.ts index 207dcc743..23dd2c21d 100644 --- a/server/src/nest/managed/managed-ext.module.ts +++ b/server/src/nest/managed/managed-ext.module.ts @@ -26,8 +26,8 @@ export class ManagedExtModule {} /** * Routes of this module that answer without a session, as `Controller.handler`. * - * Empty here, like the module. The boot gate (validate-route-guards.ts) adds - * them to its allow-list, so an install that replaces this file declares its + * Empty here, like the module. bootstrap.ts adds them to the allow-list of the + * boot gate (validate-route-guards.ts), so an install that replaces this file declares its * anonymous surface in the same place it adds it, and the gate still fails on a * route that is missing here or an entry that is stale. */