44 lines · 1.8 KB
import { z } from "zod";
import { notFoundError } from "../errors";
import { assertVisibleRevision } from "../git/reachable";
import { listRefs, resolveRevision } from "../git/refs";
import { limitWrite } from "../limits";
import { mintEditorToken } from "../editor/session";
import { publicUrl } from "../events/emit";
import { credentialAllows } from "../token-scope";
import { readJson, requireScopedRepo } from "./http";
import type { ApiContext } from "./http";

const sessionInput = z.object({ ref: z.string().min(1).max(250).optional() });

/**
 * Lets agents and CLIs join the same editor protocol as the browser: a PAT is exchanged for a one-hour,
 * ref-scoped editor token capped at the PAT's `contents` permission.
 */
export async function createEditorSession(ctx: ApiContext, owner: string, name: string): Promise<Response> {
  const data = await readJson(ctx.request, sessionInput);
  await limitWrite(ctx.viewer);
  const { repo, permission } = await requireScopedRepo(ctx, owner, name, "contents", "read");
  const revision = resolveRevision(await listRefs(repo), data.ref ?? repo.default_branch);
  if (!revision) throw notFoundError("Branch or ref not found");
  await assertVisibleRevision(repo, revision);
  const grant = ctx.credential.grant;
  const minted = await mintEditorToken(ctx.viewer, repo, revision.name, {
    ceiling: permission,
    canOpenPull: await credentialAllows(repo, ctx.credential, "pulls", "write"),
    patId: grant && grant.kind !== "deploy" ? grant.tokenId : null,
  });
  return Response.json(
    {
      token: minted.token,
      expires_at: new Date(minted.expiresAt).toISOString(),
      ref: revision.name,
      api_url: publicUrl("/api/editor/"),
      editor_url: publicUrl(
        `/${repo.owner_name}/${repo.name}/edit/${revision.name.split("/").map(encodeURIComponent).join("/")}`,
      ),
    },