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. */ | |
| 10 | const TICKET_PARAM = "ticket"; | |
| 11 | const TICKET_ROUTE = "/-/live/ticket"; | |
| 12 | ||
| 13 | let viaToken = false; | |
| 14 | ||
| 15 | /** Set by the root as it renders: whether this page was opened with an access token. */ | |
| 16 | export function setLiveViaToken(on: boolean): void { | |
| 17 | viaToken = on; | |
| 18 | } | |
| 19 | ||
| 20 | /** `wss://<this site><path>?<params>`, with a ticket when one was given. */ | |
| 21 | export 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 | ||
| 29 | async 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 | */ | |
| 51 | export 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 | } |