import type { WebSocket, WebSocketServer } from "ws"; import { verifyMeetingToken } from "./token"; import { broadcast, getParticipant, isKicked, joinRoom, kickFromRoom, leaveRoom, type Participant, } from "./rooms"; import type { ClientToServerMessage, ParticipantSummary } from "./protocol"; // A connection has this long to send its "auth" message before being // dropped. The token travels as the first WS message rather than a // `?token=` query parameter so it doesn't end up persisted in reverse-proxy // or access logs; it's still HMAC-signed and short-lived either way. const AUTH_TIMEOUT_MS = 10_000; function participantSummary(p: Participant): ParticipantSummary { return { participantId: p.participantId, name: p.name, role: p.role }; } export function attachSignalingServer(wss: WebSocketServer) { wss.on("connection", (ws: WebSocket) => { let self: Participant | null = null; let eventSlug: string | null = null; const authTimeout = setTimeout(() => { if (!self) ws.close(4000, "auth-timeout"); }, AUTH_TIMEOUT_MS); function handleAuth(token: string) { const payload = verifyMeetingToken(token); if (!payload) { ws.send(JSON.stringify({ type: "auth-error", reason: "invalid-token" })); ws.close(4000, "invalid-token"); return; } if (isKicked(payload.eventSlug, payload.participantId, payload.clientId)) { ws.close(4001, "removed-by-host"); return; } eventSlug = payload.eventSlug; self = { ws, participantId: payload.participantId, clientId: payload.clientId, name: payload.name, role: payload.role, }; const existing = joinRoom(eventSlug, self); ws.send( JSON.stringify({ type: "welcome", participantId: self.participantId, role: self.role, participants: existing.map(participantSummary), }), ); broadcast(eventSlug, { type: "peer-joined", ...participantSummary(self) }, { exclude: self.participantId, }); } ws.on("message", (raw) => { let message: ClientToServerMessage; try { message = JSON.parse(raw.toString()); } catch { return; } if (message.type === "auth") { clearTimeout(authTimeout); if (self) return; // already authenticated on this connection handleAuth(message.token); return; } if (!self || !eventSlug) return; // no messages accepted before auth if (message.type === "offer" || message.type === "answer" || message.type === "ice-candidate") { const target = getParticipant(eventSlug, message.to); if (target && target.ws.readyState === target.ws.OPEN) { target.ws.send( JSON.stringify({ type: message.type, from: self.participantId, payload: message.payload }), ); } return; } if (message.type === "kick") { if (self.role !== "host") return; // server-side authority, not client-claimed const target = getParticipant(eventSlug, message.targetParticipantId); if (!target) return; kickFromRoom(eventSlug, target.participantId); target.ws.send(JSON.stringify({ type: "kicked" })); target.ws.close(4001, "removed-by-host"); broadcast(eventSlug, { type: "peer-left", participantId: target.participantId }); } }); ws.on("close", () => { clearTimeout(authTimeout); if (!self || !eventSlug) return; leaveRoom(eventSlug, self.participantId); broadcast(eventSlug, { type: "peer-left", participantId: self.participantId }); }); }); }