186 lines · 7.4 KB
import { parseTopics } from "~/lib/topics";
import type { z } from "zod";
import { first } from "../db";
import { accountDb } from "../scoped-db";
import { badRequest, forbidden, HttpError, notFoundError, unauthorized } from "../errors";
import { publicUrl } from "../events/emit";
import { assertNotArchived, hasPermission, loadRepoFor } from "../repos";
import { scopedRepoPermission, tokenCredential } from "../token-scope";
import type { RequestCredential, TokenArea } from "../token-scope";
import { loadPull } from "../pulls";
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 noContent = (): Response => new Response(null, { status: 204 });

/**
 * Token-only: session cookies are never read, and Basic is refused because browsers replay cached Basic
 * credentials cross-site. This is why /api/v1 can skip the Worker's same-origin check.
 */
export async function apiContext(request: Request, url: URL): Promise<ApiContext> {
  const scheme = (request.headers.get("authorization") ?? "").split(" ", 1)[0].toLowerCase();
  if (scheme !== "bearer" && scheme !== "token") {
    throw unauthorized("A personal access token is required (Authorization: Bearer <token>)");
  }
  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) need an all-repositories token with admin:write. */
export function hasAccountScope(ctx: ApiContext): boolean {
  const grant = ctx.credential.grant;
  return grant?.kind === "fine" && grant.allRepos && grant.permissions.admin === "write";
}