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 { 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 { 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). */ export function scratchSiteAccount(siteId: string): string { return `scratch:site:${siteId}`; } /** Anonymous usage not tied to a site, keyed by the hashed client IP. */ export function scratchIpAccount(ipHash: string): string { return `scratch:ip:${ipHash.slice(0, 32)}`; }