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 { 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("/")}`, ), }, { status: 201 }, ); }