Skip to content
36 linesCodeBlameRaw
1import { env } from "cloudflare:workers";
2
3import { DOCS_VIEWER_HEADER } from "@g1t/contracts";
4
5import type { Route } from "./+types/live";
6import { getViewer, roleIn } from "../../../lib/session.server";
7
8/**
9 * A page's live socket: `wss://<site>/<workspace>/-/docs/live?page=<id>`.
10 * The site checks the session and that the page asking is the site's own,
11 * then hands the upgrade to the docs service with the viewer, which checks
12 * their role in the page's space and gives the socket to the page's room
13 * (one Durable Object per page), which enforces that role.
14 */
15export async function loader({ params, context, request }: Route.LoaderArgs) {
16 const viewer = getViewer(context);
17 if (!viewer) return new Response("Sign in to use Docs.", { status: 401 });
18 if (!roleIn(viewer, params.owner)) return new Response("Not found", { status: 404 });
19 if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
20 return new Response("This address takes a WebSocket.", { status: 426, headers: { upgrade: "websocket" } });
21 }
22 const origin = request.headers.get("origin");
23 if (origin && origin !== new URL(request.url).origin) return new Response("Cross-origin socket refused", { status: 403 });
24 const page = new URL(request.url).searchParams.get("page");
25 if (!page) return new Response("Which page?", { status: 400 });
26 const headers = new Headers(request.headers);
27 headers.delete("cookie");
28 headers.set(DOCS_VIEWER_HEADER, JSON.stringify(viewer));
29 const target = `https://docs/live?page=${encodeURIComponent(page)}&workspace=${encodeURIComponent(params.owner.toLowerCase())}`;
30 try {
31 return await env.DOCS.fetch(new Request(target, { method: "GET", headers }));
32 } catch (error) {
33 console.error("docs: the live socket could not be handed over", error);
34 return new Response("Docs didn't answer.", { status: 503 });
35 }
36}