49 lines · 2.3 KB
import { first } from "../db";
import { accountDb, repoDb } from "../scoped-db";
import { locate } from "../scoped-db/locators";

/**
 * Attribution rules (see docs/metering.md):
 * - Usage is charged to the owner of the repository it happens in.
 * - Fork pull request CI runs in the base repository (workflow_runs.repo_id), so the base owner pays;
 *   they approve outside-contributor runs before any cloud time is spent.
 * - Agent (MCP / OAuth) usage is charged to the human or org that authorized the agent, never the agent.
 * - Anonymous usage goes to a `scratch:` account derived from the claim token (or IP) until claimed.
 */
export async function repoOwnerAccount(repoId: string): Promise<string | null> {
  const row = await first<{ owner_id: string }>("SELECT owner_id FROM repos WHERE id = ?", repoId);
  return row?.owner_id ?? null;
}

/** The account charged for a workflow run: always the repository that runs it, including fork PRs. */
export async function ciRunAccount(runId: string): Promise<{ accountId: string; repoId: string } | null> {
  const repoId = await locate("workflow_run", runId);
  if (!repoId) return null;
  const run = await repoDb(repoId).first<{ repo_id: string }>("SELECT repo_id FROM workflow_runs WHERE id = ?", runId);
  if (!run) return null;
  const owner = await repoOwnerAccount(run.repo_id);
  return owner ? { accountId: owner, repoId: run.repo_id } : null;
}

/** The account charged for an access token's usage: the agent's authorizer, else the token's user. */
export async function tokenAccount(tokenId: string): Promise<string | null> {
  const userId = await locate("access_token_id", tokenId);
  if (!userId) return null;
  const row = await accountDb(userId).first<{ authorized_by: string | null; user_id: string }>(
    `SELECT a.authorized_by, t.user_id FROM access_tokens t LEFT JOIN mcp_token_agents a ON a.token_id = t.id
     WHERE t.id = ?`,
    tokenId,
  );
  return row ? (row.authorized_by ?? row.user_id) : null;
}

/** Anonymous scratch-site usage: one scratch account per site (1:1 with its claim token). */