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.
| Merge branch 'socket-tickets' | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { User, Viewer } from "@g1t/contracts"; | |
| 5 | ||
| A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers. | 6 | import { liveAddress, offerTicket, openLive, setLiveViaToken, takeOfferedTicket } from "./live-socket.ts"; |
| Merge branch 'socket-tickets' | 7 | import { TICKET_PARAM, TICKET_ROUTE, TICKET_SECONDS, issueTicket, openTicket, socketPath, ticketViewer } from "./socket-ticket.ts"; |
| Chat is the phone app's first part, in full and live, with notifications on the phone. Conversations draw messages as the site does, from the same Markdown parser, run one author's messages together and mark the days; new messages, edits, deletions, reactions and typing arrive over the conversation's socket, and what you send shows at once and can be sent again if it fails. Agents' cards work: their buttons act from the phone, asking first or for an amount when the card does. Long-press a message to react, reply in a thread, copy it, or edit or delete your own; threads open with the message they are under; a conversation can be starred, muted or left; New message messages people and agents or makes a channel, and Browse joins channels. While the app is in front it keeps the person's feed open as a tab does: unread counts move on the list and the Chat tab, notifications show as a banner unless that conversation is open, and DMs show who is online. The bar is the site's phone bar: Home, Notifications and Agents say they are coming and open the site, and Customize and the account move under More. | 8 | import { socketUser, tokenVerdict, websiteUser } from "./website-token.ts"; |
| Merge branch 'socket-tickets' | 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); | |
| A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers. | 159 | |
| 160 | // A ticket the page's loader minted opens the first socket with no | |
| 161 | // request; it is used once, and not when its minute is nearly up. | |
| 162 | asked.length = 0; | |
| 163 | offerTicket(CHAT, { ticket: "st1.minted", expires_at: new Date(Date.now() + 50_000).toISOString() }); | |
| 164 | const minted: string[] = []; | |
| 165 | openLive(CHAT, () => ({ channel: "ch_2" }), (address) => minted.push(address), () => false); | |
| 166 | assert.deepEqual(minted, ["wss://g1t.sh/acme/-/chat/live?channel=ch_2&ticket=st1.minted"], "synchronously, from the offered ticket"); | |
| 167 | assert.deepEqual(asked, []); | |
| 168 | const again = await new Promise<string>((resolve) => openLive(CHAT, () => ({ channel: "ch_2" }), resolve, () => false)); | |
| 169 | assert.equal(again, "wss://g1t.sh/acme/-/chat/live?channel=ch_2&ticket=st1.abc", "the next socket asks, as the offer was used"); | |
| 170 | assert.deepEqual(asked, [`/-/live/ticket?path=${encodeURIComponent(CHAT)}`]); | |
| 171 | offerTicket(CHAT, { ticket: "st1.stale", expires_at: new Date(Date.now() + 2_000).toISOString() }); | |
| 172 | assert.equal(takeOfferedTicket(CHAT), null, "an offer about to expire is not used"); | |
| 173 | offerTicket(CHAT, null); | |
| 174 | assert.equal(takeOfferedTicket(CHAT), null); | |
| Merge branch 'socket-tickets' | 175 | } finally { |
| 176 | setLiveViaToken(false); | |
| 177 | Object.defineProperty(globalThis, "location", { value: was.location, configurable: true }); | |
| 178 | globalThis.fetch = was.fetch; | |
| 179 | } | |
| 180 | }); | |
| Chat is the phone app's first part, in full and live, with notifications on the phone. Conversations draw messages as the site does, from the same Markdown parser, run one author's messages together and mark the days; new messages, edits, deletions, reactions and typing arrive over the conversation's socket, and what you send shows at once and can be sent again if it fails. Agents' cards work: their buttons act from the phone, asking first or for an amount when the card does. Long-press a message to react, reply in a thread, copy it, or edit or delete your own; threads open with the message they are under; a conversation can be starred, muted or left; New message messages people and agents or makes a channel, and Browse joins channels. While the app is in front it keeps the person's feed open as a tab does: unread counts move on the list and the Chat tab, notifications show as a banner unless that conversation is open, and DMs show who is online. The bar is the site's phone bar: Home, Notifications and Agents say they are coming and open the site, and Customize and the account move under More. | 181 | |
| 182 | test("a ticket for a chat-scoped token opens Chat's socket and the feed, checked again as it opens", async () => { | |
| 183 | const app: User = { ...ada, token: { token_id: "tok_app", scopes: ["chat:read"] } }; | |
| 184 | const tokens: Record<string, Viewer> = { g1t_app: app }; | |
| 185 | // As the site checks it: identity's answer narrowed by `socketUser` for the socket's path. | |
| 186 | const bySocket = async (token: string, path: string) => socketUser(tokens[token] ?? null, path); | |
| 187 | for (const path of [CHAT, "/-/live"]) { | |
| 188 | const { ticket } = await issueTicket(SECRET, { token: "g1t_app", userId: "usr_ada", path }, NOW); | |
| 189 | assert.equal((await ticketViewer(upgrade(path, ticket), SECRET, bySocket, NOW))?.id, "usr_ada", path); | |
| 190 | } | |
| 191 | // An artifact's socket is the website's alone, even with a genuine ticket. | |
| 192 | const artifacts = "/acme/-/artifacts/live"; | |
| 193 | const { ticket } = await issueTicket(SECRET, { token: "g1t_app", userId: "usr_ada", path: artifacts }, NOW); | |
| 194 | assert.equal(await ticketViewer(upgrade(artifacts, ticket), SECRET, bySocket, NOW), null); | |
| 195 | // Chat taken away since the ticket was made: the socket opens nothing. | |
| 196 | const chat = await issueTicket(SECRET, { token: "g1t_app", userId: "usr_ada", path: CHAT }, NOW); | |
| 197 | tokens.g1t_app = { ...app, token: { token_id: "tok_app", scopes: ["notifications:read"] } }; | |
| 198 | assert.equal(await ticketViewer(upgrade(CHAT, chat.ticket), SECRET, bySocket, NOW), null); | |
| 199 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.