Skip to content
37 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, socketUser } 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. A token let in by its
15 * `chat:read` scope rather than the website permission gets one for Chat's
16 * socket and the feed (`/-/live`) alone (lib/website-token.ts `socketUser`).
17 */
18export async function loader({ context, request }: Route.LoaderArgs) {
19 const viewer = getViewer(context);
20 const token = bearerToken(request);
21 if (!viewer || !token || !viewer.token) {
22 return Response.json(
23 { message: "Socket tickets are for pages opened with an access token; a signed-in browser's sockets use its session." },
24 { status: viewer ? 400 : 401, headers: PRIVATE },
25 );
26 }
27 const socket = socketPath(new URL(request.url).searchParams.get("path") ?? "");
28 if (!socket) return Response.json({ message: "Which socket? `path` is one of the site's live sockets." }, { status: 400, headers: PRIVATE });
29 if (!socketUser(viewer, socket.path)) {
30 return Response.json({ message: "This access token can open Chat's sockets and your feed, not this one." }, { status: 403, headers: PRIVATE });
31 }
32 if (socket.workspace && !roleIn(viewer, socket.workspace)) {
33 return Response.json({ message: "Not found" }, { status: 404, headers: PRIVATE });
34 }
35 const issued = await issueTicket(ticketSecret(), { token, userId: viewer.id, path: socket.path });
36 return Response.json(issued, { headers: PRIVATE });
37}