TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
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 { crashRoutes } from "./routes/crash";14import { arcadeRoutes } from "./routes/arcade";15import { refreshSettings } from "./lib/settings";1617const REDACT = ["req.headers.cookie", "req.headers.authorization", "*.password", "*.newPassword", "*.currentPassword", "*.recoveryCode", "*.totp", "*.passwordHash", "*.tokenHash"];1819export async function buildApp(): Promise<FastifyInstance> {20 const app = Fastify({21 logger: {22 level: config.logLevel,23 redact: { paths: REDACT, censor: "[redacted]" },24 transport: config.env === "development" ? { target: "pino-pretty", options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" } } : undefined,25 },26 trustProxy: config.trustProxy,27 bodyLimit: 64 * 1024,28 genReqId: () => Math.random().toString(36).slice(2, 10), // request ids only — never used for game outcomes29 });3031 await app.register(cookie, { secret: config.sessionSecret });32 await app.register(authPlugin);3334 app.addHook("onRequest", async (_req, reply) => {35 reply.header("Cache-Control", "no-store");36 reply.header("X-Content-Type-Options", "nosniff");37 });3839 app.addHook("onReady", async () => {40 await refreshSettings(true);41 setInterval(() => refreshSettings().catch(() => {}), 10_000).unref();42 });4344 app.setErrorHandler((err: Error & { validation?: unknown; statusCode?: number }, req, reply) => {45 if (err instanceof ApiError) {46 reply.code(err.status).send({ error: err.code, message: err.message, details: err.details });47 return;48 }49 if (err instanceof ZodError) {50 reply.code(400).send({ error: "VALIDATION", message: "Invalid request.", details: err.flatten() });51 return;52 }53 if ((err as { validation?: unknown }).validation) {54 reply.code(400).send({ error: "VALIDATION", message: err.message });55 return;56 }57 if ((err as { statusCode?: number }).statusCode === 413) {58 reply.code(413).send({ error: "PAYLOAD_TOO_LARGE", message: "Request too large." });59 return;60 }61 req.log.error({ err }, "unhandled error");62 reply.code(500).send({ error: "INTERNAL", message: "Something went wrong on our side." });63 });6465 app.setNotFoundHandler((_req, reply) => {66 reply.code(404).send({ error: "NOT_FOUND", message: "Route not found." });67 });6869 await app.register(healthRoutes);70 await app.register(authRoutes);71 await app.register(userRoutes);72 await app.register(gameRoutes);73 await app.register(rewardRoutes);74 await app.register(crashRoutes);75 await app.register(arcadeRoutes);76 await app.register(adminRoutes);7778 return app;79}80