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.
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 1 | import { env } from "cloudflare:workers"; |
| 2 | ||
| 3 | import { DOCS_VIEWER_HEADER } from "@g1t/contracts"; | |
| 4 | ||
| 5 | import type { Route } from "./+types/live"; | |
| 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. | 6 | import { roleIn } from "../../../lib/session.server"; |
| 7 | import { socketViewer } from "../../../lib/socket-ticket.server"; | |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 8 | |
| 9 | /** | |
| 10 | * An artifact's live socket: `wss://<site>/<workspace>/-/artifacts/live?folio=<id>`. | |
| 11 | * The site checks the session and that the page asking is the site's own, | |
| 12 | * then hands the upgrade to the docs service with the viewer, which checks | |
| 13 | * their role in the folio and gives the socket to its room (one Durable | |
| 14 | * Object per folio, any kind), which enforces that role. | |
| 15 | */ | |
| 16 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 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. | 17 | // A session, or a page opened with a token by its socket ticket. |
| 18 | const viewer = await socketViewer(context, request); | |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 19 | if (!viewer) return new Response("Sign in to use Artifacts.", { 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 | const origin = request.headers.get("origin"); | |
| 25 | if (origin && origin !== new URL(request.url).origin) return new Response("Cross-origin socket refused", { status: 403 }); | |
| 26 | const folio = new URL(request.url).searchParams.get("folio"); | |
| 27 | if (!folio) return new Response("Which artifact?", { status: 400 }); | |
| 28 | const headers = new Headers(request.headers); | |
| 29 | headers.delete("cookie"); | |
| 30 | headers.set(DOCS_VIEWER_HEADER, JSON.stringify(viewer)); | |
| 31 | const target = `https://docs/live?folio=${encodeURIComponent(folio)}&workspace=${encodeURIComponent(params.owner.toLowerCase())}`; | |
| 32 | try { | |
| 33 | return await env.DOCS.fetch(new Request(target, { method: "GET", headers })); | |
| 34 | } catch (error) { | |
| 35 | console.error("artifacts: the live socket could not be handed over", error); | |
| 36 | return new Response("Artifacts didn't answer.", { status: 503 }); | |
| 37 | } | |
| 38 | } |