107 lines · 3.9 KB
import { batch, first, run, stmt } from "./db";
import { randomId, randomToken, sha256Hex } from "./crypto";
import { PAT_PREFIX } from "./auth";
import { badRequest } from "./errors";
import { assertBelowCap, CAPS } from "./limits";
import { requireRepoAccess } from "./repos";
import { DEPLOY_TOKEN_PREFIX, parseTokenPermissions } from "./token-scope";
import type { TokenPermissions } from "./token-scope";
import type { RepoRow, Viewer } from "~/lib/types";

const VISIBLE_TOKEN_CHARS = 6;
const DAY_MS = 24 * 60 * 60 * 1000;

/** Only the SHA-256 of a token is stored; the plaintext is returned once to the caller. */
async function mint(prefix: string): Promise<{ token: string; hash: string; visible: string }> {
  const token = randomToken(prefix);
  return { token, hash: await sha256Hex(token), visible: token.slice(0, prefix.length + 1 + VISIBLE_TOKEN_CHARS) };
}

export function tokenExpiry(days: number): number | null {
  return days > 0 ? Date.now() + days * DAY_MS : null;
}

export type FineGrainedTokenInput = {
  name: string;
  expiresInDays: number;
  allRepos: boolean;
  repos: string[];
  permissions: TokenPermissions;
};

/** Callers must have authenticated the viewer; each selected repo must be readable by them (checked here). */
export async function createFineGrainedToken(
  viewer: Viewer,
  input: FineGrainedTokenInput,
): Promise<{ id: string; token: string; repoIds: string[] }> {
  const count = await first<{ count: number }>(
    "SELECT COUNT(*) AS count FROM access_tokens WHERE user_id = ?",
    viewer.id,
  );