91 lines · 3.4 KB
import { chunks, placeholders } from "./db";
import { accountDb, stmt, type ScopedValue } from "./scoped-db";
import { forgetLocator } from "./scoped-db/locators";
import { revokeDeployTokensMintedBy } from "./deploy-token-index";

/**
 * Revokes personal access tokens of one account with everything they minted: REST children
 * (`access_tokens.parent_token_id`), MCP delegations (`mcp_token_agents.parent_token_id`), their refresh tokens,
 * and deploy tokens they created. D1 did this in the `access_tokens_revoke_mcp` trigger; the trigger reached into
 * `deploy_tokens`, which now lives in each repository's RepoState, so the cascade runs here. Returns the ids
 * removed (roots included).
 */
export async function revokeAccessTokens(userId: string, rootIds: string[]): Promise<string[]> {
  if (rootIds.length === 0) return [];
  const db = accountDb(userId);
  const edges = await db.all<{ child: string; parent: string }>(
    `SELECT id AS child, parent_token_id AS parent FROM access_tokens WHERE parent_token_id IS NOT NULL
     UNION SELECT token_id, parent_token_id FROM mcp_token_agents WHERE parent_token_id IS NOT NULL`,
  );
  const ids = new Set(rootIds);
  let grew = true;
  while (grew) {
    grew = false;
    for (const edge of edges) {
      if (ids.has(edge.parent) && !ids.has(edge.child)) {
        ids.add(edge.child);
        grew = true;
      }
    }
  }
  const allIds = [...ids];
  const removed: { id: string; token_hash: string }[] = [];
  for (const group of chunks(allIds)) {
    removed.push(
      ...(await db.all<{ id: string; token_hash: string }>(
        `SELECT id, token_hash FROM access_tokens WHERE user_id = ? AND id IN (${placeholders(group.length)})`,
        userId,
        ...group,
      )),
    );