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). */