127 lines · 4.9 KB
import { z } from "zod";
import { badRequest, forbidden, HttpError, unauthorized } from "../errors";
import { isSafeTreePath } from "../git/writer";
import { limitBrowse, limitWrite } from "../limits";
import { json } from "./http";
import { editorRoomStub } from "./room-client";
import type { RoomJoin } from "./room";
import { canWrite, EDITOR_TOKEN_PREFIX, resolveEditorToken } from "./session";
import type { EditorSession } from "./session";
import { EDITOR_WS_PROTOCOL } from "~/lib/editor-protocol";

const MAX_EDITS = 200;
const agentName = z
  .string()
  .trim()
  .min(1)
  .max(60)
  .regex(/^[\w .@-]+$/, "Agent names may use letters, digits, spaces and . _ @ -");

function requireBranch(session: EditorSession): void {
  if (session.revision.kind !== "branch") throw badRequest("Live editing needs a branch");
}

function roomTarget(session: EditorSession) {
  return { repoId: session.repo.id, branch: session.revision.name, head: session.revision.sha };
}

/**
 * WebSocket upgrade for the co-editing room. Browsers can't set headers on sockets, so the editor token
 * travels as a second subprotocol next to `gitfor-editor.v1`; it is never put in the URL (logs, history).
 */
export async function handleRoomSocket(request: Request, url: URL): Promise<Response> {
  const protocols = (request.headers.get("sec-websocket-protocol") ?? "").split(",").map((part) => part.trim());
  if (!protocols.includes(EDITOR_WS_PROTOCOL)) throw badRequest(`Use the ${EDITOR_WS_PROTOCOL} subprotocol`);
  const token = protocols.find((part) => part.startsWith(`${EDITOR_TOKEN_PREFIX}_`));
  if (!token) throw unauthorized("An editor session token is required");
  const session = await resolveEditorToken(token);
  await limitBrowse(session.viewer, request);
  requireBranch(session);
  const agent = url.searchParams.get("agent");