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 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { User, Viewer } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { liveAddress, openLive, setLiveViaToken } from "./live-socket.ts"; | |
| 7 | import { TICKET_PARAM, TICKET_ROUTE, TICKET_SECONDS, issueTicket, openTicket, socketPath, ticketViewer } from "./socket-ticket.ts"; | |
| 8 | import { tokenVerdict, websiteUser } from "./website-token.ts"; | |
| 9 | ||
| 10 | const SECRET = "site-secret"; | |
| 11 | const NOW = Date.UTC(2026, 9, 9, 12, 0, 0); | |
| 12 | ||
| 13 | const ada: User = { | |
| 14 | id: "usr_ada", | |
| 15 | username: "ada", | |
| 16 | kind: "user", | |
| 17 | verified: true, | |
| 18 | workspaces: [{ slug: "acme", role: "owner" }], | |
| 19 | token: { token_id: "tok_web", scopes: ["repo:read"], website: true }, | |
| 20 | }; | |
| 21 | ||
| 22 | /** identity's `user_for_access_token` as the site narrows it, over a table that tests change. */ | |
| 23 | const table: Record<string, Viewer> = {}; | |
| 24 | const lookup = async (token: string) => websiteUser(table[token] ?? null); | |
| 25 | ||
| 26 | function reset() { | |
| 27 | for (const key of Object.keys(table)) delete table[key]; | |
| 28 | table.g1t_web = ada; | |
| 29 | } | |
| 30 | ||
| 31 | function upgrade(path: string, ticket: string | null, headers: Record<string, string> = { upgrade: "websocket" }): Request { | |
| 32 | const url = new URL(`https://g1t.sh${path}`); | |
| 33 | if (ticket) url.searchParams.set("ticket", ticket); | |
| 34 | return new Request(url, { headers }); | |
| 35 | } | |
| 36 | ||
| 37 | const CHAT = "/acme/-/chat/live"; | |
| 38 | ||
| 39 | test("only the site's live sockets take a ticket, in the form the routes match", () => { | |
| 40 | assert.deepEqual(socketPath("/-/live"), { path: "/-/live", workspace: null }); | |
| 41 | assert.deepEqual(socketPath("/Acme//-/chat/live/"), { path: "/acme/-/chat/live", workspace: "acme" }); | |
| 42 | assert.deepEqual(socketPath("/acme/-/artifacts/live"), { path: "/acme/-/artifacts/live", workspace: "acme" }); | |
| 43 | for (const path of ["/", "/acme", "/acme/-/chat", "/acme/-/inbox/live", "/-/-/chat/live", "/acme/web/-/chat/live", "/-/live/ticket", "/%E0%A4%A"]) { | |
| 44 | assert.equal(socketPath(path), null, path); | |
| 45 | } | |
| 46 | }); | |
| 47 | ||
| 48 | test("a ticket opens the socket it was made for, as the token's owner", async () => { | |
| 49 | reset(); | |
| 50 | const { ticket, expires_at } = await issueTicket(SECRET, { token: "g1t_web", userId: "usr_ada", path: CHAT }, NOW); | |
| 51 | assert.match(ticket, /^st1\.[A-Za-z0-9_-]+$/); | |
| 52 | assert.equal(expires_at, new Date(NOW + TICKET_SECONDS * 1000).toISOString()); | |
| 53 | assert.ok(!ticket.includes("g1t_web"), "the token is not readable in the ticket"); | |
| 54 | assert.deepEqual(await openTicket(SECRET, ticket, CHAT, NOW + 1000), { token: "g1t_web", userId: "usr_ada" }); | |
| 55 | const viewer = await ticketViewer(upgrade(`${CHAT}?channel=ch_1`, ticket), SECRET, lookup, NOW + 1000); | |
| 56 | assert.equal(viewer?.id, "usr_ada"); | |
| 57 | // The path as the browser may spell it. | |
| 58 | assert.equal((await ticketViewer(upgrade("/ACME/-/chat/live/", ticket), SECRET, lookup, NOW))?.id, "usr_ada"); | |
| 59 | }); | |
| 60 | ||
| 61 | test("a ticket past its minute opens nothing", async () => { | |
| 62 | reset(); | |
| 63 | const { ticket } = await issueTicket(SECRET, { token: "g1t_web", userId: "usr_ada", path: CHAT }, NOW); | |
| 64 | assert.ok(await openTicket(SECRET, ticket, CHAT, NOW + (TICKET_SECONDS - 1) * 1000)); | |
| 65 | assert.equal(await openTicket(SECRET, ticket, CHAT, NOW + TICKET_SECONDS * 1000), null); | |
| 66 | assert.equal(await ticketViewer(upgrade(CHAT, ticket), SECRET, lookup, NOW + 5 * 60_000), null); | |
| 67 | }); | |
| 68 | ||
| 69 | test("a ticket opens no other socket: another kind, or another workspace's", async () => { | |
| 70 | reset(); | |
| 71 | const { ticket } = await issueTicket(SECRET, { token: "g1t_web", userId: "usr_ada", path: CHAT }, NOW); | |
| 72 | for (const path of ["/acme/-/artifacts/live", "/-/live", "/other/-/chat/live"]) { | |
| 73 | assert.equal(await openTicket(SECRET, ticket, path, NOW), null, path); | |
| 74 | assert.equal(await ticketViewer(upgrade(path, ticket), SECRET, lookup, NOW), null, path); | |
| 75 | } | |
| 76 | }); | |
| 77 | ||
| 78 | test("a ticket changed in any way, or sealed with another key, opens nothing", async () => { | |
| 79 | reset(); | |
| 80 | const { ticket } = await issueTicket(SECRET, { token: "g1t_web", userId: "usr_ada", path: CHAT }, NOW); | |
| 81 | const body = ticket.slice(4); | |
| 82 | for (let i = 0; i < body.length; i += 7) { | |
| 83 | const swapped = body[i] === "A" ? "B" : "A"; | |
| 84 | const tampered = `st1.${body.slice(0, i)}${swapped}${body.slice(i + 1)}`; | |
| 85 | assert.equal(await openTicket(SECRET, tampered, CHAT, NOW), null, `byte ${i}`); | |
| 86 | } | |
| 87 | for (const bad of ["", "st1.", "st1.!!!", `st2.${body}`, body, `st1.${body}x`, `st1.${"A".repeat(3000)}`]) { | |
| 88 | assert.equal(await openTicket(SECRET, bad, CHAT, NOW), null, bad.slice(0, 20)); | |
| 89 | } | |
| 90 | assert.equal(await openTicket("another-secret", ticket, CHAT, NOW), null); | |
| 91 | }); | |
| 92 | ||
| 93 | test("the token is checked again when the socket opens: deleted, revoked or without the website permission, it opens nothing", async () => { | |
| 94 | reset(); | |
| 95 | const { ticket } = await issueTicket(SECRET, { token: "g1t_web", userId: "usr_ada", path: CHAT }, NOW); | |
| 96 | // Deleted, expired or revoked: identity knows it no more. | |
| 97 | delete table.g1t_web; | |
| 98 | assert.equal(await ticketViewer(upgrade(CHAT, ticket), SECRET, lookup, NOW), null); | |
| 99 | // "Use the website as you" turned off since the page loaded. | |
| 100 | table.g1t_web = { ...ada, token: { token_id: "tok_web", scopes: ["repo:read"], website: false } }; | |
| 101 | assert.equal(await ticketViewer(upgrade(CHAT, ticket), SECRET, lookup, NOW), null); | |
| 102 | // A token that now names someone else. | |
| 103 | table.g1t_web = { ...ada, id: "usr_bob", username: "bob" }; | |
| 104 | assert.equal(await ticketViewer(upgrade(CHAT, ticket), SECRET, lookup, NOW), null); | |
| 105 | // A ticket made for something that is not a token. | |
| 106 | const odd = await issueTicket(SECRET, { token: "a".repeat(64), userId: "usr_ada", path: CHAT }, NOW); | |
| 107 | table["a".repeat(64)] = ada; | |
| 108 | assert.equal(await ticketViewer(upgrade(CHAT, odd.ticket), SECRET, lookup, NOW), null); | |
| 109 | }); | |
| 110 | ||
| 111 | test("a ticket is never taken by anything but a socket's upgrade", async () => { | |
| 112 | reset(); | |
| 113 | const { ticket } = await issueTicket(SECRET, { token: "g1t_web", userId: "usr_ada", path: CHAT }, NOW); | |
| 114 | // The socket's own address, asked without an upgrade. | |
| 115 | assert.equal(await ticketViewer(upgrade(CHAT, ticket, {}), SECRET, lookup, NOW), null); | |
| 116 | assert.equal(await ticketViewer(upgrade(CHAT, ticket, { upgrade: "h2c" }), SECRET, lookup, NOW), null); | |
| 117 | // Pages, data requests and form posts: the token rules see no token, so the session cookie (or no one) decides. | |
| 118 | for (const path of ["/acme", "/acme/-/chat", "/acme/-/chat.data", "/settings/tokens"]) { | |
| 119 | const page = new Request(`https://g1t.sh${path}?ticket=${encodeURIComponent(ticket)}`); | |
| 120 | assert.deepEqual(await tokenVerdict(page, async () => ada), { kind: "none" }, path); | |
| 121 | const post = new Request(`https://g1t.sh${path}?ticket=${encodeURIComponent(ticket)}`, { method: "POST", body: new FormData() }); | |
| 122 | assert.deepEqual(await tokenVerdict(post, async () => ada), { kind: "none" }, path); | |
| 123 | } | |
| 124 | // An upgrade without a ticket is the session's business, as before. | |
| 125 | assert.equal(await ticketViewer(upgrade(CHAT, null), SECRET, lookup, NOW), null); | |
| 126 | }); | |
| 127 | ||
| 128 | test("a session's page opens its sockets at once and adds no ticket; a token's page asks for one first", async () => { | |
| 129 | const site = new URL("https://g1t.sh/acme/-/chat"); | |
| 130 | const was = { location: globalThis.location, fetch: globalThis.fetch }; | |
| 131 | Object.defineProperty(globalThis, "location", { value: site, configurable: true }); | |
| 132 | const asked: string[] = []; | |
| 133 | globalThis.fetch = (async (input: string) => { | |
| 134 | asked.push(String(input)); | |
| 135 | return Response.json({ ticket: "st1.abc", expires_at: "2026-10-09T12:01:00.000Z" }); | |
| 136 | }) as typeof fetch; | |
| 137 | try { | |
| 138 | assert.equal(liveAddress(CHAT, { channel: "ch_1" }, null), "wss://g1t.sh/acme/-/chat/live?channel=ch_1"); | |
| 139 | ||
| 140 | setLiveViaToken(false); | |
| 141 | const opened: string[] = []; | |
| 142 | openLive(CHAT, () => ({ channel: "ch_1" }), (address) => opened.push(address), () => false); | |
| 143 | assert.deepEqual(opened, ["wss://g1t.sh/acme/-/chat/live?channel=ch_1"], "synchronously, with no ticket"); | |
| 144 | assert.deepEqual(asked, []); | |
| 145 | ||
| 146 | setLiveViaToken(true); | |
| 147 | const viaToken = await new Promise<string>((resolve) => openLive("/-/live", () => ({ workspace: "acme" }), resolve, () => false)); | |
| 148 | assert.equal(viaToken, "wss://g1t.sh/-/live?workspace=acme&ticket=st1.abc"); | |
| 149 | assert.deepEqual(asked, ["/-/live/ticket?path=%2F-%2Flive"]); | |
| 150 | // The page's copy of the route and parameter, as the server has them. | |
| 151 | assert.equal(TICKET_ROUTE, "/-/live/ticket"); | |
| 152 | assert.equal(TICKET_PARAM, "ticket"); | |
| 153 | ||
| 154 | // Closed while the ticket was on its way: nothing opens. | |
| 155 | let late = false; | |
| 156 | openLive(CHAT, () => ({}), () => (late = true), () => true); | |
| 157 | await new Promise((resolve) => setTimeout(resolve, 10)); | |
| 158 | assert.equal(late, false); | |
| 159 | } finally { | |
| 160 | setLiveViaToken(false); | |
| 161 | Object.defineProperty(globalThis, "location", { value: was.location, configurable: true }); | |
| 162 | globalThis.fetch = was.fetch; | |
| 163 | } | |
| 164 | }); |