119 lines · 4.6 KB
import type { z } from "zod";
import { first } from "../db";
import { badRequest, forbidden, HttpError, notFoundError, unauthorized } from "../errors";
import { publicUrl } from "../events/emit";
import { hasPermission, loadRepo } from "../repos";
import { scopedRepoPermission, tokenCredential } from "../token-scope";
import type { RequestCredential, TokenArea } from "../token-scope";
import type { PullRow } from "../pulls";
import type { Permission, RepoRow, Viewer } from "~/lib/types";

export type ApiContext = { request: Request; url: URL; credential: RequestCredential; viewer: Viewer };

export function unprocessable(message: string): HttpError {
  return new HttpError(422, message);
}

export const notImplemented = (): Response => Response.json({ error: "not implemented" }, { status: 501 });

export const noContent = (): Response => new Response(null, { status: 204 });

/** Token-only: the API never honours session cookies, so it carries no ambient browser authority. */
export async function apiContext(request: Request, url: URL): Promise<ApiContext> {
  const credential = await tokenCredential(request);
  if (!credential?.viewer) throw unauthorized("A valid personal access token is required");
  return { request, url, credential, viewer: credential.viewer };
}

/** Account-level actions (creating repos, managing tokens) are outside any fine-grained repo grant. */
export function requireAccountScope(ctx: ApiContext): void {
  const grant = ctx.credential.grant;
  if (grant?.kind === "classic") return;
  if (grant?.kind === "fine" && grant.allRepos && grant.permissions.admin === "write") return;
  throw forbidden("This token cannot perform account-level actions");
}

export async function readJson<T>(request: Request, schema: z.ZodType<T>): Promise<T> {
  let raw: unknown;
  try {
    const text = await request.text();
    raw = text.trim() === "" ? {} : JSON.parse(text);