import { accountDb, repoDb } from "../scoped-db"; import type { RepoRow } from "~/lib/types"; export type OwnerVisibility = "all" | "private" | "selected"; type EncryptedRow = { name: string; ciphertext: string; iv: string }; type OwnerScoped = { visibility: OwnerVisibility; selected_repos: string }; /** Owner-level secrets and variables apply to all repos, only private repos, or an explicit allow-list. */ export function ownerScopeAllows(row: OwnerScoped, repo: Pick): boolean { if (row.visibility === "all") return true; if (row.visibility === "private") return repo.visibility === "private"; const selected: unknown = JSON.parse(row.selected_repos); return Array.isArray(selected) && selected.includes(repo.id); } export async function environmentIdFor(repoId: string, name: string | null | undefined): Promise { if (!name) return null; const row = await repoDb(repoId).first<{ id: string }>( "SELECT id FROM environments WHERE repo_id = ? AND name = ?", repoId, name, ); return row?.id ?? null; } /** The `vars` context: environment variables override repository variables, which override owner variables. */ export async function loadVariables(repo: RepoRow, environmentId: string | null): Promise> { const db = repoDb(repo.id); const [owner, repoRows, envRows] = await Promise.all([ accountDb(repo.owner_id).all( "SELECT name, value, visibility, selected_repos FROM owner_variables WHERE owner_id = ?", repo.owner_id, ), db.all<{ name: string; value: string }>("SELECT name, value FROM repo_variables WHERE repo_id = ?", repo.id), environmentId ? db.all<{ name: string; value: string }>( "SELECT name, value FROM environment_variables WHERE environment_id = ?", environmentId, ) : Promise.resolve([]), ]); const values: Record = {}; for (const row of owner.filter((candidate) => ownerScopeAllows(candidate, repo))) values[row.name] = row.value; for (const row of repoRows) values[row.name] = row.value; for (const row of envRows) values[row.name] = row.value; return values; } /** Encrypted secrets visible to a job, with the same environment > repository > owner precedence. */ export async function loadSecretRows(repo: RepoRow, environmentId: string | null): Promise { const db = repoDb(repo.id); const [owner, repoRows, envRows] = await Promise.all([ accountDb(repo.owner_id).all( "SELECT name, ciphertext, iv, visibility, selected_repos FROM owner_secrets WHERE owner_id = ?", repo.owner_id, ), db.all("SELECT name, ciphertext, iv FROM secrets WHERE repo_id = ?", repo.id), environmentId ? db.all( "SELECT name, ciphertext, iv FROM environment_secrets WHERE environment_id = ?", environmentId, ) : Promise.resolve([]), ]); const merged = new Map(); for (const row of owner.filter((candidate) => ownerScopeAllows(candidate, repo))) merged.set(row.name, row); for (const row of [...repoRows, ...envRows]) merged.set(row.name, row); return [...merged.values()]; }