Skip to content
32 linesCodeBlameRaw
1import type { Route } from "./+types/ticket";
2import { getViewer, roleIn } from "../../lib/session.server";
3import { issueTicket, socketPath } from "../../lib/socket-ticket";
4import { ticketSecret } from "../../lib/socket-ticket.server";
5import { bearerToken } from "../../lib/website-token";
6
7const PRIVATE = { "cache-control": "no-store", "x-robots-tag": "noindex" };
8
9/**
10 * A socket ticket for a page opened with an access token:
11 * `GET /-/live/ticket?path=/<workspace>/-/chat/live` answers
12 * `{ ticket, expires_at }`, good for a minute on that socket alone
13 * (lib/socket-ticket.ts). Only a request signed in by a token gets one: a
14 * session's sockets carry its cookie and need none.
15 */
16export async function loader({ context, request }: Route.LoaderArgs) {
17 const viewer = getViewer(context);
18 const token = bearerToken(request);
19 if (!viewer || !token || !viewer.token?.website) {
20 return Response.json(
21 { message: "Socket tickets are for pages opened with an access token; a signed-in browser's sockets use its session." },
22 { status: viewer ? 400 : 401, headers: PRIVATE },
23 );
24 }
25 const socket = socketPath(new URL(request.url).searchParams.get("path") ?? "");
26 if (!socket) return Response.json({ message: "Which socket? `path` is one of the site's live sockets." }, { status: 400, headers: PRIVATE });
27 if (socket.workspace && !roleIn(viewer, socket.workspace)) {
28 return Response.json({ message: "Not found" }, { status: 404, headers: PRIVATE });
29 }
30 const issued = await issueTicket(ticketSecret(), { token, userId: viewer.id, path: socket.path });
31 return Response.json(issued, { headers: PRIVATE });
32}