import type { SessionSnapshot } from "../shared/types"; import type { BunRequest } from "bun"; import { applySnapshot, joinSession, snapshotSession } from "./domain"; import { createSession, createTestPreview, createTestSession, getSession, getSessionByCode, isTestSessionCode, } from "./store"; import { broadcastSessionState, startTestSimulation } from "./websocket"; function jsonResponse(data: unknown, status = 200): Response { return Response.json(data, { status }); } async function readJson(req: Request): Promise { return (await req.json()) as T; } function previewSession(session: SessionSnapshot) { return { sessionId: session.id, code: session.code, status: session.status, players: session.players.map((player) => ({ id: player.id, name: player.name, role: player.role, isDummy: player.isDummy, connected: player.connected, })), }; } function readBankerId(req: Request): string | null { const url = new URL(req.url); return url.searchParams.get("bankerId"); } function isSnapshotCandidate(value: unknown): value is SessionSnapshot { if (!value || typeof value !== "object") return false; const snapshot = value as SessionSnapshot; return ( typeof snapshot.id === "string" && typeof snapshot.code === "string" && typeof snapshot.status === "string" && typeof snapshot.createdAt === "number" && typeof snapshot.bankerId === "string" && typeof snapshot.blackoutActive === "boolean" && Array.isArray(snapshot.players) && Array.isArray(snapshot.transactions) && Array.isArray(snapshot.chats) && Array.isArray(snapshot.groups) && Array.isArray(snapshot.takeoverRequests) ); } export const apiRoutes = { "/api/health": { GET() { return jsonResponse({ ok: true }); }, }, "/api/session": { async POST(req: BunRequest) { let body: { bankerName?: string }; try { body = await readJson<{ bankerName?: string }>(req); } catch { return jsonResponse({ message: "Invalid request body" }, 400); } const bankerName = body.bankerName?.trim() || "Banker"; const { session, banker } = createSession(bankerName); return jsonResponse({ sessionId: session.id, sessionCode: session.code, playerId: banker.id, role: banker.role, status: session.status, }); }, }, "/api/session/:code/join": { async POST(req: BunRequest) { const code = req.params.code; let body: { name?: string; playerId?: string }; try { body = await readJson<{ name?: string; playerId?: string }>(req); } catch { return jsonResponse({ message: "Invalid request body" }, 400); } if (isTestSessionCode(code)) { const session = createTestSession(); const player = joinSession(session, body.name ?? "Player", body.playerId); startTestSimulation(session.id); return jsonResponse({ sessionId: session.id, sessionCode: session.code, playerId: player.id, role: player.role, status: session.status, }); } const session = getSessionByCode(code) ?? getSession(code); if (!session) { return jsonResponse({ message: "Session not found" }, 404); } if (session.status === "ended") { return jsonResponse({ message: "Session has ended" }, 400); } const player = joinSession(session, body.name ?? "Player", body.playerId); return jsonResponse({ sessionId: session.id, sessionCode: session.code, playerId: player.id, role: player.role, status: session.status, }); }, }, "/api/session/:id": { GET(req: BunRequest) { const session = getSession(req.params.id); if (!session) { return jsonResponse({ message: "Session not found" }, 404); } const snapshot: SessionSnapshot = snapshotSession(session); return jsonResponse(snapshot); }, }, "/api/session/:id/state": { GET(req: BunRequest) { const session = getSession(req.params.id) ?? getSessionByCode(req.params.id); if (!session) { return jsonResponse({ message: "Session not found" }, 404); } const bankerId = readBankerId(req); if (!bankerId || bankerId !== session.bankerId) { return jsonResponse({ message: "Not authorized" }, 403); } const snapshot: SessionSnapshot = snapshotSession(session); return jsonResponse(snapshot); }, async POST(req: BunRequest) { const session = getSession(req.params.id) ?? getSessionByCode(req.params.id); if (!session) { return jsonResponse({ message: "Session not found" }, 404); } let body: Record; try { body = await readJson>(req); } catch { return jsonResponse({ message: "Invalid request body" }, 400); } const bankerId = (typeof body.bankerId === "string" ? body.bankerId : null) ?? readBankerId(req); if (!bankerId || bankerId !== session.bankerId) { return jsonResponse({ message: "Not authorized" }, 403); } const candidate = (body.state ?? body) as unknown; if (!isSnapshotCandidate(candidate)) { return jsonResponse({ message: "Invalid session snapshot" }, 400); } applySnapshot(session, candidate); broadcastSessionState(session.id); return jsonResponse({ ok: true }); }, }, "/api/session/:code/info": { GET(req: BunRequest) { const code = req.params.code; if (isTestSessionCode(code)) { const snapshot = createTestPreview(); return jsonResponse(previewSession(snapshot)); } const session = getSessionByCode(code) ?? getSession(code); if (!session) { return jsonResponse({ message: "Session not found" }, 404); } const snapshot: SessionSnapshot = snapshotSession(session); return jsonResponse(previewSession(snapshot)); }, }, };