import './instrument'; // BigInt cannot be serialized by JSON.stringify by default. // Polyfill toJSON so Express/NestJS can serialize Prisma BigInt fields. (BigInt.prototype as any).toJSON = function () { return this.toString(); }; import { RequestMethod, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; import { LoggerService, RedisIoAdapter, validateEnv } from '@modules/shared'; import { AppModule } from './app.module'; async function bootstrap() { validateEnv(); const app = await NestFactory.create(AppModule, { bufferLogs: true, rawBody: true, bodyParser: true, }); const logger = app.get(LoggerService); app.useLogger(logger); // ── API Versioning — global /api/v1/ prefix ── app.setGlobalPrefix('api/v1', { exclude: [ { path: 'health', method: RequestMethod.GET }, { path: 'health/(.*)', method: RequestMethod.GET }, ], }); // ── OpenAPI / Swagger ── const swaggerConfig = new DocumentBuilder() .setTitle('Goodgo Platform API') .setDescription('Real-estate platform API — listings, search, payments, subscriptions, analytics') .setVersion('1.0') .addBearerAuth( { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'JWT', ) .addTag('auth', 'Authentication & user profile') .addTag('listings', 'Property listings CRUD & moderation') .addTag('search', 'Full-text & geo search') .addTag('payments', 'Payment processing & callbacks') .addTag('subscriptions', 'Plans, billing & usage metering') .addTag('admin', 'Admin panel operations') .addTag('notifications', 'Notification history & preferences') .addTag('analytics', 'Market reports & price analytics') .addTag('reviews', 'Property reviews & ratings') .addTag('mcp', 'Model Context Protocol server transport') .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('api/v1/docs', app, document, { swaggerOptions: { persistAuthorization: true }, jsonDocumentUrl: 'api/v1/docs-json', }); // ── WebSocket Adapter (Socket.IO) ── // Redis pub/sub fan-out for multi-instance broadcasts; falls back to the // in-memory IoAdapter when Redis is unreachable (single-node / local dev). const wsAdapter = new RedisIoAdapter(app); await wsAdapter.connectToRedis(); app.useWebSocketAdapter(wsAdapter); // ── Security Headers (Helmet) ── app.use( helmet({ // CSP relaxed for API — responses are consumed cross-origin by the web frontend contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'], styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'], imgSrc: ["'self'", 'data:', 'https:', 'blob:'], connectSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://api.goodgo.vn', 'wss:', 'ws:'], fontSrc: ["'self'", 'data:'], objectSrc: ["'none'"], frameSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], }, }, // Must allow cross-origin for API consumed by platform.goodgo.vn crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: false, crossOriginResourcePolicy: { policy: 'cross-origin' }, frameguard: { action: 'deny' }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, }), ); // ── Permissions-Policy Header ── app.use((_req: unknown, res: { setHeader: (name: string, value: string) => void }, next: () => void) => { res.setHeader( 'Permissions-Policy', 'camera=(), microphone=(), geolocation=(self), payment=(self)', ); next(); }); // ── Cookie Parser (required for CSRF double-submit pattern) ── app.use(cookieParser()); // ── CORS ── const corsOrigins = process.env['CORS_ORIGINS']; if (!corsOrigins && process.env['NODE_ENV'] === 'production') { throw new Error('CORS_ORIGINS must be set in production'); } const allowedOrigins = (corsOrigins ?? 'http://localhost:3000') .split(',') .map((o) => o.trim()); app.enableCors({ origin: allowedOrigins, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Correlation-Id', 'X-CSRF-Token'], exposedHeaders: ['X-Correlation-Id'], credentials: true, maxAge: 86400, }); // ── Global Validation Pipe ── app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, transformOptions: { enableImplicitConversion: true }, disableErrorMessages: process.env['NODE_ENV'] === 'production', }), ); // ── Request Body Size Limit ── const expressApp = app.getHttpAdapter().getInstance(); // ── Trust Proxy (for rate limiting behind reverse proxy) ── expressApp.set('trust proxy', 1); // ── Graceful Shutdown ── app.enableShutdownHooks(); const port = process.env['PORT'] ?? 3001; await app.listen(port); logger.log(`API running on http://localhost:${port}/api/v1`, 'Bootstrap'); } bootstrap();