1import Fastify, { type FastifyInstance } from "fastify";2import cookie from "@fastify/cookie";3import { ZodError } from "zod";4import { config } from "./config";5import { ApiError } from "./lib/errors";6import { authPlugin } from "./plugins/auth";7import { authRoutes } from "./routes/auth";8import { userRoutes } from "./routes/user";9import { gameRoutes } from "./routes/games";10import { rewardRoutes } from "./routes/rewards";11import { healthRoutes } from "./routes/health";12import { adminRoutes } from "./routes/admin";13import { refreshSettings } from "./lib/settings";1415const REDACT = ["req.headers.cookie", "req.headers.authorization", "*.password", "*.newPassword", "*.currentPassword", "*.recoveryCode", "*.totp", "*.passwordHash", "*.tokenHash"];1617export async function buildApp(): Promise<FastifyInstance> {18 const app = Fastify({19 logger: {20 level: config.logLevel,21 redact: { paths: REDACT, censor: "[redacted]" },22 transport: config.env === "development" ? { target: "pino-pretty", options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" } } : undefined,23 },24 trustProxy: config.trustProxy,25 bodyLimit: 64 * 1024,26 genReqId: () => Math.random().toString(36).slice(2, 10), // request ids only — never used for game outcomes27 });2829 await app.register(cookie, { secret: config.sessionSecret });30 await app.register(authPlugin);3132 app.addHook("onRequest", async (_req, reply) => {33 reply.header("Cache-Control", "no-store");34 reply.header("X-Content-Type-Options", "nosniff");35 });3637 app.addHook("onReady", async () => {38 await refreshSettings(true);39 setInterval(() => refreshSettings().catch(() => {}), 10_000).unref();40 });4142 app.setErrorHandler((err: Error & { validation?: unknown; statusCode?: number }, req, reply) => {43 if (err instanceof ApiError) {44 reply.code(err.status).send({ error: err.code, message: err.message, details: err.details });45 return;46 }47 if (err instanceof ZodError) {48 reply.code(400).send({ error: "VALIDATION", message: "Invalid request.", details: err.flatten() });49 return;50 }51 if ((err as { validation?: unknown }).validation) {52 reply.code(400).send({ error: "VALIDATION", message: err.message });53 return;54 }55 if ((err as { statusCode?: number }).statusCode === 413) {56 reply.code(413).send({ error: "PAYLOAD_TOO_LARGE", message: "Request too large." });57 return;58 }59 req.log.error({ err }, "unhandled error");60 reply.code(500).send({ error: "INTERNAL", message: "Something went wrong on our side." });61 });6263 app.setNotFoundHandler((_req, reply) => {64 reply.code(404).send({ error: "NOT_FOUND", message: "Route not found." });65 });6667 await app.register(healthRoutes);68 await app.register(authRoutes);69 await app.register(userRoutes);70 await app.register(gameRoutes);71 await app.register(rewardRoutes);72 await app.register(adminRoutes);7374 return app;75}76