import 'reflect-metadata'; import { randomUUID } from 'node:crypto'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import type { NextFunction, Request, Response } from 'express'; import helmet from 'helmet'; import { AppModule } from './app.module.js'; import { ApiErrorFilter } from './common/api-error.filter.js'; import { APP_CONFIG, type AppConfig } from './config/app-config.module.js'; import { AppLogger } from './logging/app-logger.js'; type RequestWithId = Request & { requestId?: string; }; export async function createApplication() { const app = await NestFactory.create(AppModule, { bodyParser: false, // 自定义日志器安装前保留 Nest 启动日志,避免配置错误没有可见诊断。 logger: ['error', 'warn', 'log'], }); const config = app.get(APP_CONFIG); const logger = new AppLogger(config.LOG_LEVEL); app.useLogger(logger); if (config.trustedProxyCidrs.length > 0) { // 仅在部署明确声明代理网段时信任转发头,避免伪造客户端 IP。 app.getHttpAdapter() .getInstance() .set('trust proxy', config.trustedProxyCidrs); } app.enableCors({ origin: (origin, callback) => { // Extension API calls are bearer-token authenticated. Allow only Chrome // extension origins in addition to the configured Web application. if (!origin || origin === config.WEB_ORIGIN || /^chrome-extension:\/\/[a-p]{32}$/.test(origin)) { callback(null, true); return; } callback(null, false); }, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Authorization', 'Content-Type', 'Idempotency-Key', 'If-Match', 'X-Atlas-Platform-Id', 'X-Request-Id', ], exposedHeaders: ['X-Request-Id'], }); // Helmet、body 限制、请求 ID 与全局错误过滤器必须在业务路由前注册。 app.use(helmet({ contentSecurityPolicy: false })); // 批量佣金文件可达 2 MiB,专用入口预留元数据空间,其他 API 保留原限制。 app.useBodyParser('json', { limit: 3 * 1024 * 1024, type: (req) => (req as Request).path.startsWith('/api/system/pricing/commissions') && Boolean((req as Request).is('application/json')), }); app.useBodyParser('json', { limit: config.API_BODY_LIMIT_BYTES }); app.useBodyParser('urlencoded', { limit: config.API_BODY_LIMIT_BYTES, extended: true, }); app.use( (request: RequestWithId, response: Response, next: NextFunction) => { const typedRequest = request; const typedResponse = response; const suppliedId = typedRequest.headers['x-request-id']; // 合法调用方提供的 request ID 可贯穿上游链路;其他情况由服务端生成。 const requestId = typeof suppliedId === 'string' && /^[A-Za-z0-9_-]{8,128}$/.test(suppliedId) ? suppliedId : `req_${randomUUID()}`; const startedAt = performance.now(); typedRequest.requestId = requestId; typedResponse.setHeader('X-Request-Id', requestId); typedResponse.on('finish', () => { logger.log('http_request_completed', { request_id: requestId, method: typedRequest.method, path: typedRequest.originalUrl, status_code: typedResponse.statusCode, duration_ms: Math.round(performance.now() - startedAt), }); }); next(); }, ); app.useGlobalFilters(new ApiErrorFilter(logger)); app.setGlobalPrefix('api', { exclude: ['health', 'ready'] }); app.enableShutdownHooks(); if (config.swaggerEnabled) { // Swagger 仅描述运行时已注册路由;冻结接口契约仍以 docs/api 为准。 const swaggerConfig = new DocumentBuilder() .setTitle('Atlas Seller Kit API') .setDescription('Atlas 卖家工作台 HTTP API 文档') .setVersion('0.1.0') .addBearerAuth( { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'access-token', ) .build(); const swaggerDocument = SwaggerModule.createDocument( app, swaggerConfig, ); SwaggerModule.setup('api-docs', app, swaggerDocument, { swaggerOptions: { persistAuthorization: true }, }); } return { app, config, logger }; } export async function bootstrap() { const { app, config, logger } = await createApplication(); await app.listen(config.API_PORT); logger.log('api_started', { port: config.API_PORT, swagger_enabled: config.swaggerEnabled, }); } // 测试复用应用工厂时不自动监听固定端口,只有直接运行入口才启动服务。 if ( process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) ) { void bootstrap().catch((error: unknown) => { const details = error instanceof Error ? { name: error.name, message: error.message, stack: error.stack, } : error; console.error( JSON.stringify({ timestamp: new Date().toISOString(), level: 'error', message: 'api_startup_failed', error: details, }), ); process.exitCode = 1; }); }