174 lines · 7.3 KB
import { z } from "zod";
import { first } from "../db";
import { repoDb } from "../scoped-db";
import { forbidden, notFoundError } from "../errors";
import { applyLabels, indexIssue, issueUrl, loadIssue } from "../issues";
import type { IssueRow } from "../issues";
import { addIssueEvent } from "../pulls";
import { limitWrite } from "../limits";
import { assertNotArchived, hasPermission } from "../repos";
import { credentialCoversRepo } from "../token-scope";
import { changeIssueState, commentsWithAuthors, insertComment, insertIssue } from "../mcp/issue-writes";
import { markdownBody, title } from "~/lib/validation";
import type { RepoRow } from "~/lib/types";
import { readJson, requireScopedRepo, unprocessable } from "./http";
import type { ApiContext, ScopedRepo } from "./http";

const LIST_LIMIT = 100;
const MAX_COMMENTS = 100;

/** Read access lets a session write on public repositories; a token must also list the repository. */
async function requireIssueWriteCoverage(ctx: ApiContext, repoId: string): Promise<void> {
  if (!(await credentialCoversRepo(ctx.credential, repoId)))
    throw forbidden("This token does not cover this repository");
}

/** Opening issues and commenting need only read access (as on the web), but a fine-grained token must opt in to issue writes. */
function requireIssueWriteGrant(ctx: ApiContext): void {
  const grant = ctx.credential.grant;
  if (grant?.kind === "fine" && grant.permissions.issues !== "write") {
    throw forbidden("This token cannot write issues");
  }
  if (grant?.kind === "deploy") throw forbidden("Deploy keys cannot write issues");
}

function canMaintain(scoped: ScopedRepo): boolean {
  return hasPermission(scoped.permission, "write");
}

async function loadPlainIssue(repo: RepoRow, number: number): Promise<IssueRow> {
  const issue = await loadIssue(repo.id, number);