import Fastify, { type FastifyInstance } from "fastify"; import cookie from "@fastify/cookie"; import { ZodError } from "zod"; import { config } from "./config"; import { ApiError } from "./lib/errors"; import { authPlugin } from "./plugins/auth"; import { authRoutes } from "./routes/auth"; import { userRoutes } from "./routes/user"; import { gameRoutes } from "./routes/games"; import { rewardRoutes } from "./routes/rewards"; import { healthRoutes } from "./routes/health"; import { adminRoutes } from "./routes/admin"; import { crashRoutes } from "./routes/crash"; import { arcadeRoutes } from "./routes/arcade"; import { refreshSettings } from "./lib/settings"; const REDACT = ["req.headers.cookie", "req.headers.authorization", "*.password", "*.newPassword", "*.currentPassword", "*.recoveryCode", "*.totp", "*.passwordHash", "*.tokenHash"]; export async function buildApp(): Promise { const app = Fastify({ logger: { level: config.logLevel, redact: { paths: REDACT, censor: "[redacted]" }, transport: config.env === "development" ? { target: "pino-pretty", options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" } } : undefined, }, trustProxy: config.trustProxy, bodyLimit: 64 * 1024, genReqId: () => Math.random().toString(36).slice(2, 10), // request ids only — never used for game outcomes }); await app.register(cookie, { secret: config.sessionSecret }); await app.register(authPlugin); app.addHook("onRequest", async (_req, reply) => { reply.header("Cache-Control", "no-store"); reply.header("X-Content-Type-Options", "nosniff"); }); app.addHook("onReady", async () => { await refreshSettings(true); setInterval(() => refreshSettings().catch(() => {}), 10_000).unref(); }); app.setErrorHandler((err: Error & { validation?: unknown; statusCode?: number }, req, reply) => { if (err instanceof ApiError) { reply.code(err.status).send({ error: err.code, message: err.message, details: err.details }); return; } if (err instanceof ZodError) { reply.code(400).send({ error: "VALIDATION", message: "Invalid request.", details: err.flatten() }); return; } if ((err as { validation?: unknown }).validation) { reply.code(400).send({ error: "VALIDATION", message: err.message }); return; } if ((err as { statusCode?: number }).statusCode === 413) { reply.code(413).send({ error: "PAYLOAD_TOO_LARGE", message: "Request too large." }); return; } req.log.error({ err }, "unhandled error"); reply.code(500).send({ error: "INTERNAL", message: "Something went wrong on our side." }); }); app.setNotFoundHandler((_req, reply) => { reply.code(404).send({ error: "NOT_FOUND", message: "Route not found." }); }); await app.register(healthRoutes); await app.register(authRoutes); await app.register(userRoutes); await app.register(gameRoutes); await app.register(rewardRoutes); await app.register(crashRoutes); await app.register(arcadeRoutes); await app.register(adminRoutes); return app; }