// MUST be the very first import — see apps/portal-bff/src/observability/tracing.ts // for the reasoning. Anything `import`ed above this line bypasses the // OpenTelemetry auto-instrumentations and is silently un-traced. import './observability/tracing'; import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import { Logger } from 'nestjs-pino'; import { AppModule } from './app/app.module'; import { assertDatabaseUrl } from './config/check-database-url'; import { assertEntraConfig } from './config/check-entra-config'; import { assertSessionSecret } from './config/check-session-secret'; // Fail fast on a malformed DATABASE_URL (most often a special char in // the password that needs URL-encoding) rather than letting Prisma // surface a cryptic "invalid connection string" error mid-request. assertDatabaseUrl(); // Same family of pre-flight check for the Entra app-registration env // vars (per ADR-0009). Missing / placeholder values fail here rather // than deep inside the first auth request. assertEntraConfig(); // SESSION_SECRET signs the auth-flow cookies (pre-auth state + // PKCE verifier today, session cookie next). const sessionSecret = assertSessionSecret(); async function bootstrap() { // `bufferLogs: true` holds early-bootstrap log lines until the // Pino-based Logger is wired in below, so we don't lose anything // emitted before `app.useLogger()`. const app = await NestFactory.create(AppModule, { bufferLogs: true }); app.useLogger(app.get(Logger)); // CORS — minimal dev-time allowlist. The SPA running on // http://localhost:4200 issues fetches to the BFF and must be // able to send the W3C `traceparent` (and `tracestate`) headers // that `@opentelemetry/instrumentation-fetch` injects, so the BFF // can pick up the parent span id and emit child spans on the same // trace. The full security-grade allowlist (per-environment // origins, credentials policy, helmet stack, etc.) lands with the // phase-2 security ADR — for now this is the minimum needed for // end-to-end tracing. app.enableCors({ origin: (process.env['CORS_ALLOWED_ORIGINS'] ?? 'http://localhost:4200') .split(',') .map((o) => o.trim()) .filter(Boolean), allowedHeaders: ['Content-Type', 'Accept', 'Authorization', 'traceparent', 'tracestate'], credentials: true, }); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); // Cookie parsing for the auth flow (per ADR-0009). The same // `SESSION_SECRET` will cover the post-login session cookie when // ADR-0010 storage lands; signed cookies are read from // `req.signedCookies`, unsigned from `req.cookies`. app.use(cookieParser(sessionSecret)); // Phase-2 security ADRs will harden the above: helmet, real CORS // allowlist, CSRF protection, rate limiting, auth guards, // structured error filter. const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); const port = process.env['PORT'] ?? 3000; await app.listen(port); app .get(Logger) .log(`Application is running on: http://localhost:${port}/${globalPrefix}`, 'Bootstrap'); } bootstrap();