212 lines · 8.1 KB
import { assertAccountActive } from "../account-disable";
import { accountDb, stmt, type ScopedStatement } from "../scoped-db";
import { forgetLocator, locate, registerLocator } from "../scoped-db/locators";
import { randomId, sha256Hex, toHex } from "../crypto";
import { accountToken } from "../account-scope";
import { revokeAccessTokens, revokeAccessTokensWhere } from "../access-token-store";
import { PAT_PREFIX } from "../auth";
import { assertBelowCap, CAPS } from "../limits";
import type { TokenPermissions } from "../token-scope";

const VISIBLE_TOKEN_CHARS = 6;
/** OAuth-issued tokens are tracked separately so agents cannot crowd out a user's personal tokens. */
const OAUTH_TOKENS_PER_USER = 100;

export type GrantSpec = {
  permissions: TokenPermissions;
  allRepos: boolean;
  repoIds: string[];
  agent: string;
};

export type AgentKind = "registered" | "oauth" | "delegated" | "unregistered";

export type MintedToken = { id: string; token: string; expiresAt: number | null };

function newPat(userId: string): string {
  return accountToken(PAT_PREFIX, userId, toHex(crypto.getRandomValues(new Uint8Array(20))));
}

/**
 * Issues an ordinary fine-grained personal access token (`access_tokens` + `access_token_repos`) and
 * records the agent it belongs to, so OAuth grants inherit every existing permission check unchanged.
 */
export async function mintFineGrainedToken(input: {
  userId: string;
  name: string;
  scopes: string;
  grant: GrantSpec;
  ttlMs: number | null;
  clientId: string | null;