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.
| Chat and workspace agents: channels, DMs and named agents you talk to | 1 | import { env } from "cloudflare:workers"; |
| 2 | ||
| 3 | import { CHAT_VIEWER_HEADER } from "@g1t/contracts"; | |
| 4 | ||
| 5 | import type { Route } from "./+types/live"; | |
| Merge branch 'socket-tickets' | 6 | import { roleIn } from "../../../lib/session.server"; |
| 7 | import { socketViewer } from "../../../lib/socket-ticket.server"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 8 | |
| 9 | /** | |
| 10 | * A conversation's live socket: `wss://<site>/<workspace>/-/chat/live?channel=<id>`. | |
| 11 | * The site checks the session and that the page asking is the site's own, | |
| 12 | * then hands the upgrade to the chat service with the viewer, which checks | |
| 13 | * they may read the channel and keeps the socket (one Durable Object per | |
| 14 | * channel, hibernating while nothing happens). | |
| 15 | */ | |
| 16 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| Merge branch 'socket-tickets' | 17 | // A session, or a page opened with a token by its socket ticket. |
| 18 | const viewer = await socketViewer(context, request); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 19 | if (!viewer) return new Response("Sign in to use chat.", { status: 401 }); |
| 20 | if (!roleIn(viewer, params.owner)) return new Response("Not found", { status: 404 }); | |
| 21 | if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") { | |
| 22 | return new Response("This address takes a WebSocket.", { status: 426, headers: { upgrade: "websocket" } }); | |
| 23 | } | |
| 24 | // Only the site's own pages may open it: a page elsewhere carries the | |
| 25 | // cookie too, so the origin is what tells them apart. | |
| 26 | const origin = request.headers.get("origin"); | |
| 27 | if (origin && origin !== new URL(request.url).origin) return new Response("Cross-origin socket refused", { status: 403 }); | |
| 28 | const channel = new URL(request.url).searchParams.get("channel"); | |
| 29 | if (!channel) return new Response("Which channel?", { status: 400 }); | |
| 30 | const headers = new Headers(request.headers); | |
| 31 | // Neither the session nor anything else of the browser's goes on. | |
| 32 | headers.delete("cookie"); | |
| 33 | headers.set(CHAT_VIEWER_HEADER, JSON.stringify(viewer)); | |
| 34 | headers.set("x-g1t-workspace", params.owner.toLowerCase()); | |
| 35 | const target = `https://chat/live?channel=${encodeURIComponent(channel)}&workspace=${encodeURIComponent(params.owner.toLowerCase())}`; | |
| 36 | try { | |
| 37 | return await env.CHAT.fetch(new Request(target, { method: "GET", headers })); | |
| 38 | } catch (error) { | |
| 39 | console.error("chat: the live socket could not be handed over", error); | |
| 40 | return new Response("Chat didn't answer.", { status: 503 }); | |
| 41 | } | |
| 42 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.