import "dotenv/config"; import * as Sentry from "@sentry/node"; import { NestFactory } from "@nestjs/core"; import { ValidationPipe } from "@nestjs/common"; import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import { Logger } from "nestjs-pino"; import helmet from "helmet"; import { AppModule } from "./app.module"; import { SentryExceptionFilter } from "./common/sentry.filter"; async function bootstrap() { const isProduction = process.env.NODE_ENV === "production"; // ─── Sentry initialization (before app creation) ────────────────────────── if (process.env.SENTRY_DSN) { Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV ?? "development", tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? 0), }); } const app = await NestFactory.create(AppModule, { bufferLogs: true, rawBody: true, // Required for Stripe webhook signature verification }); // ─── Security headers ───────────────────────────────────────────────────── const scriptSrc = isProduction ? ["'self'"] : ["'self'", "'unsafe-inline'"]; const styleSrc = isProduction ? ["'self'"] : ["'self'", "'unsafe-inline'"]; app.use( helmet({ crossOriginEmbedderPolicy: false, contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc, styleSrc, imgSrc: ["'self'", "data:", "https:"], objectSrc: ["'none'"], baseUri: ["'self'"], frameAncestors: ["'none'"], }, }, }), ); // ─── CORS ───────────────────────────────────────────────────────────────── const corsOrigin = process.env.CORS_ORIGIN ?? "http://localhost:3052"; app.enableCors({ origin: corsOrigin.split(",").map((o) => o.trim()), methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"], credentials: true, }); // ─── Global prefix ──────────────────────────────────────────────────────── app.setGlobalPrefix("api"); // ─── Global validation pipe ─────────────────────────────────────────────── app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, transformOptions: { enableImplicitConversion: true }, }), ); // ─── Swagger / OpenAPI ──────────────────────────────────────────────────── if (process.env.NODE_ENV !== "production") { const config = new DocumentBuilder() .setTitle("LedgerOne API") .setDescription("Personal finance & bookkeeping SaaS API") .setVersion("1.0") .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup("api/docs", app, document); } // ─── Global exception filter (Sentry + structured error response) ───────── app.useGlobalFilters(new SentryExceptionFilter()); // ─── Use Pino as the logger ──────────────────────────────────────────────── app.useLogger(app.get(Logger)); const port = process.env.PORT ?? 3051; await app.listen(port); app.get(Logger).log(`LedgerOne backend running on port ${port}`, "Bootstrap"); } bootstrap();