178 lines · 7.1 KB
import { assertAccountActive } from "./account-disable";
import { captureServerEvent } from "./analytics";
import { accountDb, repoDb, stmt } from "./scoped-db";
import { forgetLocator, registerLocator } from "./scoped-db/locators";
import { accountToken } from "./account-scope";
import { notifySecurityEvent } from "./account-security";
import { randomId, sha256Hex, toHex } from "./crypto";
import { PAT_PREFIX } from "./auth";
import { badRequest } from "./errors";
import { assertBelowCap, CAPS } from "./limits";
import { recordDeployToken } from "./deploy-token-index";
import { requireRepoAccess } from "./repos";
import { assertAreaDependencies, 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. The token embeds the id
 * of the account or repository that stores it, so lookups skip the locator.
 */
async function mint(prefix: string, scopeId: string): Promise<{ token: string; hash: string; visible: string }> {
  const token = accountToken(prefix, scopeId, toHex(crypto.getRandomValues(new Uint8Array(20))));
  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 = {