137 lines · 5.4 KB
import { batch, first, run, stmt } from "./db";
import { notifySecurityEvent } from "./account-security";
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;
}

/** Expiry for a token minted by another token: never later than the parent's own expiry. */
export function cappedTokenExpiry(days: number, notAfter: number | null | undefined): number | null {
  const requested = tokenExpiry(days);
  if (notAfter === null || notAfter === undefined) return requested;
  return requested === null ? notAfter : Math.min(requested, notAfter);
}

export type TokenLineageInput = {
  /** The token that minted this one; it is revoked along with it (AUTH-A1). */
  parentTokenId: string;
  /** The parent's expiry, a hard ceiling on the child's. */
  notAfter: number | null;
};

export type FineGrainedTokenInput = {
  name: string;