import { randomUUID } from "node:crypto"; import type { HttpErrorResponseDTO } from "@metis/core/common"; import { registerCoreRoutes } from "@metis/core/module"; import type { Application, ErrorRequestHandler, RequestHandler } from "express"; import express from "express"; import helmet from "helmet"; import type { HttpLogger } from "./infrastructure/http/http-logger"; import { logHttpRequest } from "./infrastructure/http/http-logger"; export function createApp( logger: HttpLogger = logHttpRequest, identity?: { middleware: RequestHandler; register: (app: Application) => void } ) { const app = express(); app.disable("x-powered-by"); app.use(helmet()); app.use((request, response, next) => { const requestId = randomUUID(); const startedAt = performance.now(); response.setHeader("x-request-id", requestId); response.setHeader("cache-control", "no-store"); response.once("finish", () => { logger({ event: "http_request", requestId, method: request.method, statusCode: response.statusCode, durationMs: Math.round((performance.now() - startedAt) * 100) / 100, }); }); next(); }); app.use(express.json({ limit: "64kb" })); if (identity) { app.use(identity.middleware); identity.register(app); } registerCoreRoutes(app); app.use((_request, response) => { const error: HttpErrorResponseDTO = { code: "NOT_FOUND", requestId: response.getHeader("x-request-id")?.toString() ?? "", }; response.status(404).json(error); }); const errorHandler: ErrorRequestHandler = (failure: unknown, _request, response, next) => { if (response.headersSent) { next(failure); return; } const status = bodyFailureStatus(failure); const error: HttpErrorResponseDTO = { code: status === 500 ? "INTERNAL_ERROR" : "INVALID_REQUEST", requestId: response.getHeader("x-request-id")?.toString() ?? "", }; response.status(status).json(error); }; app.use(errorHandler); return app; } function bodyFailureStatus(failure: unknown): number { if (!(failure instanceof Error && "type" in failure)) { return 500; } // Solo se reconocen errores públicos del parser, nunca mensajes o status arbitrarios. switch (failure.type) { case "entity.parse.failed": case "request.aborted": case "request.size.invalid": return 400; case "entity.too.large": return 413; case "charset.unsupported": case "encoding.unsupported": return 415; default: return 500; } }