// src/hooks.server.ts import type { Handle, HandleServerError } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import { building } from '$app/environment'; import * as Sentry from '@sentry/sveltekit'; if (!building) { Sentry.init({ dsn: process.env.PUBLIC_SENTRY_DSN, tracesSampleRate: 1.0, // TODO: lower to 0.2–0.3 once past alpha environment: process.env.NODE_ENV ?? 'development', sendDefaultPii: true }); } import { auth } from '$lib/server/auth'; import { svelteKitHandler } from 'better-auth/svelte-kit'; import { db } from '$lib/server/db'; import { user as userTable } from '$lib/server/db/auth.schema'; import { eq } from 'drizzle-orm'; import { ensureRootAdminRole } from '$lib/server/permissions'; import { checkRateLimit } from '$lib/server/rate-limit'; import { log } from '$lib/server/logger'; import { metrics } from '$lib/server/metrics'; // ── Initialize the IRC DM handler once on server startup ── if (!building) { import('$lib/server/bancho/dm-handler') .then(({ initDMHandler }) => initDMHandler()) .catch((err) => log.dm.warn({ err: err.message }, 'DM handler init skipped')); import('$lib/server/tournament/scheduler') .then(({ startTournamentScheduler }) => startTournamentScheduler()) .catch((err) => log.tournament.warn({ err: err.message }, 'Tournament scheduler init skipped')); import('$lib/server/discord/client') .then(({ initDiscordClient }) => initDiscordClient()) .catch((err) => log.discord.warn({ err: err.message }, 'Discord client init skipped')); process.on('unhandledRejection', (reason) => { log.http.error({ err: reason }, 'Unhandled rejection'); }); process.on('uncaughtException', (err) => { log.http.fatal({ err }, 'Uncaught exception'); }); } /** Safe IP getter — returns null for internal requests missing the ADDRESS_HEADER */ function getIp(event: Parameters[0]['event']): string | null { try { return event.getClientAddress(); } catch { return null; } } // ── Request logging middleware ── // Paths that are too noisy to log every time (healthchecks, polling) const SILENT_PATHS = new Set(['/api/health', '/api/queue']); const handleRequestLogging: Handle = async ({ event, resolve }) => { const requestId = crypto.randomUUID(); const start = performance.now(); event.locals.requestId = requestId; const response = await resolve(event); const duration = Math.round(performance.now() - start); const path = event.url.pathname; const status = response.status; // Always count metrics, but only log interesting requests metrics.requestCount++; metrics.totalResponseTime += duration; const isSilent = SILENT_PATHS.has(path) && status < 400; if (!isSilent) { const userId = event.locals.user?.id; log.http.info( { requestId, method: event.request.method, path, status, duration, userId: userId ?? null, ip: getIp(event) }, `${event.request.method} ${path} ${status} ${duration}ms` ); } if (duration > 1000) { log.http.warn({ requestId, path, duration }, 'Slow request'); metrics.slowRequests++; } response.headers.set('X-Request-Id', requestId); return response; }; // ── Auth + rate limiting ── const handleBetterAuth: Handle = async ({ event, resolve }) => { // Rate limiting for API routes if (event.url.pathname.startsWith('/api/')) { const ip = getIp(event) ?? 'unknown'; // Stricter limit for auth endpoints: 10 req/min per IP if (event.url.pathname.startsWith('/api/auth/')) { const { ok, retryAfter } = checkRateLimit(`auth:${ip}`, 10, 60_000); if (!ok) { return new Response('Too Many Requests', { status: 429, headers: { 'Retry-After': String(retryAfter ?? 60), 'Content-Type': 'text/plain' } }); } } // General API limit: 600 req/min per IP const { ok, retryAfter } = checkRateLimit(`api:${ip}`, 600, 60_000); if (!ok) { return new Response('Too Many Requests', { status: 429, headers: { 'Retry-After': String(retryAfter ?? 60), 'Content-Type': 'text/plain' } }); } } const session = await auth.api.getSession({ headers: event.request.headers }); if (session) { event.locals.session = session.session; const dbUser = await db.query.user.findFirst({ where: eq(userTable.id, session.user.id) }); let role = dbUser?.role ?? 'player'; if (dbUser) { role = await ensureRootAdminRole({ id: dbUser.id, email: dbUser.email, role }); } event.locals.user = { ...session.user, role }; } return svelteKitHandler({ event, resolve, auth, building }); }; export const handle: Handle = sequence( Sentry.sentryHandle(), handleRequestLogging, handleBetterAuth ); // ── Global server error handler ── export const handleError: HandleServerError = Sentry.handleErrorWithSentry(({ error, event }) => { const err = error instanceof Error ? error : new Error(String(error)); log.http.error( { err, requestId: event.locals.requestId, path: event.url.pathname, method: event.request.method }, 'Unhandled server error' ); return { message: 'An unexpected error occurred.' }; });