Skip to content
64 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works.1/**
2 * Opening the site's live sockets from the browser. A page signed in by a
3 * session opens them at once, its cookie going along as always. A page
4 * opened with an access token has no cookie, and a socket cannot carry the
5 * token's header, so it first asks for a socket ticket and adds it to the
6 * address (lib/socket-ticket.ts). Browser-only.
7 */
8
9/** As lib/socket-ticket.ts's, kept apart so the page does not load its sealing code. */
10const TICKET_PARAM = "ticket";
11const TICKET_ROUTE = "/-/live/ticket";
12
13let viaToken = false;
14
15/** Set by the root as it renders: whether this page was opened with an access token. */
16export function setLiveViaToken(on: boolean): void {
17 viaToken = on;
18}
19
20/** `wss://<this site><path>?<params>`, with a ticket when one was given. */
21export function liveAddress(path: string, params: Record<string, string | null | undefined>, ticket: string | null): string {
22 const url = new URL(path, location.href);
23 url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
24 for (const [name, value] of Object.entries(params)) if (value != null && value !== "") url.searchParams.set(name, value);
25 if (ticket) url.searchParams.set(TICKET_PARAM, ticket);
26 return url.toString();
27}
28
29async function ticketFor(path: string): Promise<string | null> {
30 try {
31 const answer = await fetch(`${TICKET_ROUTE}?path=${encodeURIComponent(path)}`, {
32 headers: { accept: "application/json" },
33 cache: "no-store",
34 credentials: "same-origin",
35 });
36 if (!answer.ok) return null;
37 const body = (await answer.json()) as { ticket?: unknown };
38 return typeof body.ticket === "string" ? body.ticket : null;
39 } catch {
40 return null;
41 }
42}
43
44/**
45 * Calls `open` with the address of the socket at `path`: at once for a
46 * session, after fetching a fresh ticket for a token's page (each attempt
47 * gets its own, as one lasts a minute). `params` is read when the address
48 * is made, so it sees any change meanwhile. Nothing is opened once
49 * `cancelled` says so.
50 */
51export function openLive(
52 path: string,
53 params: () => Record<string, string | null | undefined>,
54 open: (address: string) => void,
55 cancelled: () => boolean,
56): void {
57 if (!viaToken) {
58 open(liveAddress(path, params(), null));
59 return;
60 }
61 void ticketFor(path).then((ticket) => {
62 if (!cancelled()) open(liveAddress(path, params(), ticket));
63 });
64}