Ctrl K
ComunidadGuías…

server/src/main.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { ValidationPipe } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { Logger } from 'nestjs-pino';
import type { ConfigType } from '@nestjs/config';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';
import { join } from 'path';
import fastifyCookie from '@fastify/cookie';
import fastifyMultipart from '@fastify/multipart';
import fastifyStatic from '@fastify/static';
import fastifyHelmet from '@fastify/helmet';
import fastifyCompress from '@fastify/compress';
import type { FastifyInstance, FastifyReply } from 'fastify';
import { appConfig } from './config/config';
import { DEV_CLIENT_ORIGIN } from './config/dev-client-origin';
import { setupSwaggerDocs } from './swagger';
import {
  parseBooleanEnv,
  parseTrustProxy,
  buildHelmetOptions,
  buildEmptyJsonBodyStream,
  markEmptyBodyInjected,
  registerConditionalHsts,
  registerDeclaredBodyLimits,
  registerDeclaredCompressionOptOuts,
  registerEmptyBodyContentTypeParser,
  shouldInjectEmptyJsonBody,
  shouldServeSpaFallback,
} from './common/utils/bootstrap.utils';
import { applyStaticCacheHeaders } from './common/utils/static-cache.utils';

const MAX_COVER_BYTES = 20 * 1024 * 1024;

async function bootstrap() {
  const allowCloudflareInsights = parseBooleanEnv(process.env.CSP_ALLOW_CLOUDFLARE_INSIGHTS, false);

  const adapter = new FastifyAdapter({ logger: false, trustProxy: parseTrustProxy(process.env.TRUST_PROXY), maxParamLength: 1000 });
  // Raw JSON bytes are what App Attest assertions hash for routes that take a body.
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, adapter, { bufferLogs: true, rawBody: true });
  app.useLogger(app.get(Logger));

  const fastify = adapter.getInstance();
  // Nest adds originalUrl to the raw request, but these helpers only use standard Fastify APIs.
  const standardFastify = fastify as unknown as FastifyInstance;

  registerDeclaredBodyLimits(fastify);
  registerDeclaredCompressionOptOuts(fastify);

  // Fastify's default JSON parser rejects empty bodies, so we inject '{}' before parsing.
  fastify.addHook('preParsing', (request, _reply, payload, done) => {
    if (shouldInjectEmptyJsonBody(request.method, request.headers)) {
      markEmptyBodyInjected(request);
      done(null, buildEmptyJsonBodyStream(request.headers));
      return;
    }
    done(null, payload);
  });
  // Reverse proxies can forward empty mutating requests with chunked transfer or an unsupported content type.
  registerEmptyBodyContentTypeParser(standardFastify);

  // Echo pino-http's request ID so clients can correlate errors with server logs.
  fastify.addHook('onSend', (_request, reply, _payload, done) => {
    const id = reply.request.id;
    if (id !== undefined && id !== null) {
      void reply.header('X-Request-Id', String(id));
    }
    done();
  });

  app.useWebSocketAdapter(new IoAdapter(app));

  app.setGlobalPrefix('api/v1', {
    exclude: ['api/kobo/:deviceToken/(.*)', 'api/v3/(.*)', 'api/UserStorage/(.*)'],
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  app.useGlobalFilters(new GlobalExceptionFilter());

  const appConfiguration = app.get<ConfigType<typeof appConfig>>(appConfig.KEY);
  if (appConfiguration.swaggerEnabled) {
    await setupSwaggerDocs(app, appConfiguration);
  }

  await app.register(fastifyHelmet as never, buildHelmetOptions({ allowCloudflareInsights }));
  registerConditionalHsts(standardFastify);

  await app.register(fastifyCompress as never, { encodings: ['gzip', 'br'] });

  await app.register(fastifyCookie as never);
  await app.register(fastifyMultipart as never, { limits: { fileSize: MAX_COVER_BYTES } });

  if (process.env.NODE_ENV !== 'production') {
    app.enableCors({
      origin: process.env.CLIENT_URL ?? DEV_CLIENT_ORIGIN,
      credentials: true,
    });
  }

  if (process.env.NODE_ENV === 'production') {
    // NestJS calls setNotFoundHandler during init — intercept it so non-API 404s
    // fall back to index.html instead of returning a JSON 404 (SPA routing support).
    const adapterAny = adapter as any;
    adapterAny.setNotFoundHandler = (nestHandler: (req: unknown, res: unknown) => void) => {
      fastify.setNotFoundHandler(async (request, reply) => {
        if (request.url.startsWith('/api')) {
          return nestHandler(request, reply);
        }
        if (!shouldServeSpaFallback(request.url)) {
          return reply.status(404).send({ statusCode: 404, message: 'Not Found', path: request.url });
        }
        // Nest's SWC type checker does not retain @fastify/static's reply augmentation
        // through the adapter callback, even though the plugin decorates this reply at runtime.
        const staticReply = reply as typeof reply & { sendFile(filename: string): typeof reply };
        return staticReply.sendFile('index.html');
      });
    };

    const publicRoot = join(__dirname, '..', 'public');
    await app.register(fastifyStatic as never, {
      root: publicRoot,
      prefix: '/',
      setHeaders: (reply: FastifyReply, filePath: string) => applyStaticCacheHeaders(reply, publicRoot, filePath),
    });
  }

  app.enableShutdownHooks();
  await app.listen(process.env.PORT ?? 3000, appConfiguration.host);
}

bootstrap().catch((err: unknown) => {
  const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
  process.stderr.write(`BookOrbit startup failed:\n${message}\n`);
  process.exit(1);
});