import { parseCookie } from "cookie"; import type { Socket } from "socket.io"; import { env } from "../config/env.js"; import { resolveAdminFromToken } from "./require-admin.js"; /** * `socket.io` handshake middleware guarding the admin WebSocket surface — * the connection-level equivalent of {@link requireAdmin} (`require- * admin.ts`). A `socket.io` handshake is a plain HTTP request that never * runs through Express's own middleware stack (`cookie-parser` included), * so the raw `Cookie` header has to be parsed by hand here with the * `cookie` package (the same one `cookie-parser` itself wraps). * * Registered via `io.use(requireAdminSocket)` — `socket.io` calls every * `io.use` middleware once per connection attempt, before `"connection"` * fires, exactly like Express's per-request middleware chain. Calling * `next()` with no argument admits the connection; calling it with an * `Error` rejects it, which the client sees as a `connect_error` event * (`socket.io-client` never even fires `"connect"` for it). * * On success, stashes the resolved admin on `socket.data.adminUser` — * `socket.io`'s per-connection storage bag, the WebSocket equivalent of * `res.locals` — for any handler that ends up needing it later. */ export async function requireAdminSocket( socket: Socket, next: (err?: Error) => void, ): Promise { try { const cookieHeader = socket.handshake.headers.cookie; const token = cookieHeader ? parseCookie(cookieHeader)[env.ADMIN_COOKIE_NAME] : undefined; if (typeof token !== "string") { throw new Error("Not authenticated"); } socket.data.adminUser = await resolveAdminFromToken(token); next(); } catch { // Covers everything resolveAdminFromToken can throw, plus a missing // cookie — same "never distinguish the reason" posture as requireAdmin. next(new Error("Not authenticated")); } }