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("/")}`,
),
},