23 lines · 1.0 KB
import { all, chunks, placeholders, run } from "../db";

const DAY_MS = 24 * 60 * 60 * 1000;
/** Dynamically registered clients that never completed an authorization are garbage after this long. */
const UNUSED_DCR_CLIENT_MS = 7 * DAY_MS;
const PRUNE_BATCH = 500;

/**
 * Keeps the OAuth directory bounded (INT-A24): dynamic-client registrations nobody ever used. Codes and refresh
 * tokens expire inside each account's AccountState (account-state/housekeeping.ts). A client with an unexpired
 * refresh token can't match: issuing or rotating one sets `last_used_at`, and codes live a minute.
 */
export async function runMcpCron(now = Date.now()): Promise<void> {
  const unused = await all<{ id: string }>(
    "SELECT id FROM oauth_clients WHERE kind = 'dcr' AND last_used_at IS NULL AND created_at < ? LIMIT ?",
    now - UNUSED_DCR_CLIENT_MS,
    PRUNE_BATCH,
  );
  for (const ids of chunks(unused.map((row) => row.id))) {
    await run(`DELETE FROM oauth_clients WHERE id IN (${placeholders(ids.length)})`, ...ids);
  }
}