import { advertisement, artifactsSource, relayPack, RelayScreenRejected } from "../git/relay"; import type { RelaySource } from "../git/relay"; import { receivePackBody } from "../git/ref-sync-protocol"; import type { AdvertisedRef, RefCommand } from "../git/ref-sync-protocol"; import { triggerPush } from "../events/push-trigger"; import { isPolicyRefusal, screenMirrorPush, withoutHiddenRefs } from "./mirror-screen"; import type { MirrorPolicy } from "./mirror-screen"; import { pushToArtifacts } from "../git/writer"; import { hasPermission, loadRepoById, repoPermission } from "../repos"; import { loadUserById, toViewer } from "../auth"; import { badRequest } from "../errors"; import { gitAuthorization, githubApi, installationToken } from "./github-app"; import type { OutboundResult } from "./outbound"; import { parseConfig } from "./store"; import type { IntegrationRow } from "./store"; import type { DeliverInput } from "./deliver"; import type { RepoRow } from "~/lib/types"; const MAX_MIRROR_REFS = 1000; const MAX_MIRROR_BYTES = 1024 * 1024 * 1024; const ZERO_SHA = "0".repeat(40); const GITHUB_REPO = /^[A-Za-z0-9-]{1,39}\/[A-Za-z0-9._-]{1,100}$/; export type MirrorResult = { updated: number; rejected: string[]; skipped: number }; export function isGithubRepoName(value: string): boolean { return GITHUB_REPO.test(value) && !value.includes(".."); } /** Refs to send so `target` matches `source`; never deletes, and caps the count so one sync stays bounded. */ export function planMirror( source: AdvertisedRef[], target: AdvertisedRef[], ): { commands: RefCommand[]; skipped: number } { const current = new Map(target.map((ref) => [ref.name, ref.sha])); const changed = source.filter((ref) => current.get(ref.name) !== ref.sha); const commands = changed.slice(0, MAX_MIRROR_REFS).map((ref) => ({ name: ref.name, sha: ref.sha, oldSha: current.get(ref.name) ?? ZERO_SHA, })); return { commands, skipped: changed.length - commands.length }; } async function githubRemote(row: IntegrationRow): Promise { const config = parseConfig(row); if (!config.githubInstallationId || !config.githubRepo || !isGithubRepoName(config.githubRepo)) { throw new Error("The GitHub mirror is missing an installation or repository"); } const token = await installationToken(config.githubInstallationId); return { url: `https://github.com/${config.githubRepo}.git`, authorization: gitAuthorization(token) }; } export type CollaboratorPermission = { permission?: string; user?: { permissions?: { push?: boolean; admin?: boolean } }; }; /** Whether GitHub's collaborator-permission answer lets this login push. */ export function collaboratorCanPush(result: CollaboratorPermission): boolean { const level = result.permission ?? "none"; return ( level === "admin" || level === "write" || level === "maintain" || result.user?.permissions?.push === true || result.user?.permissions?.admin === true ); } /** Static part of the push-mirror gate: the installer recorded push on exactly this repo at install time. */ export function mirrorPushConfigured(config: { githubLogin?: string; githubInstallationId?: number; githubRepo?: string; githubPushRepos?: string[]; }): { ok: true; login: string; installationId: number; repo: string } | { ok: false; reason: string } { const repo = config.githubRepo ?? ""; if (!config.githubLogin || !config.githubInstallationId || !repo) { return { ok: false, reason: "Push mirroring requires reinstalling the GitHub integration to record who may push" }; } if (!(config.githubPushRepos ?? []).includes(repo.toLowerCase())) { return { ok: false, reason: `${repo} is not a GitHub repository you can push to` }; } return { ok: true, login: config.githubLogin, installationId: config.githubInstallationId, repo }; } /** * The installation token can write to every repo the App covers, so a push mirror is only allowed when the * person who configured it (a) still has write on the forge repo and (b) still has push on the GitHub repo. * Neither the visibility list captured at install time nor the App's own permission is enough on its own. */ export async function assertMirrorPushAllowed(row: IntegrationRow, repo: RepoRow): Promise { const config = parseConfig(row); const configured = mirrorPushConfigured(config); if (!configured.ok) throw badRequest(configured.reason); const user = row.created_by ? await loadUserById(row.created_by) : null; if (!user || !hasPermission(await repoPermission(repo, toViewer(user)), "write")) { throw badRequest("The user who configured this mirror no longer has write access to this repository"); } const token = await installationToken(configured.installationId); const result = await githubApi( token, `/repos/${configured.repo}/collaborators/${encodeURIComponent(configured.login)}/permission`, ); if (!collaboratorCanPush(result)) throw badRequest(`${configured.login} can no longer push to ${configured.repo}`); } /** * Pull-mirror gate on what GitHub reports now: a private GitHub repo needs the installer to still read it, * and its code may never land in a forge repo that is not private (that would publish it). */ export function mirrorPullDecision(input: { githubPrivate: boolean; forgeVisibility: string; installerPermission: string | null; }): { ok: true } | { ok: false; reason: string } { if (!input.githubPrivate) return { ok: true }; const level = input.installerPermission ?? "none"; if (level === "none") return { ok: false, reason: "The user who configured this mirror can no longer read it on GitHub (or must reinstall)", }; if (input.forgeVisibility !== "private") return { ok: false, reason: "A private GitHub repository can only be mirrored into a private repository" }; return { ok: true }; } /** * Re-verified on every pull and every issue import page: the installer's forge write access and their current * access on GitHub, and a private GitHub source only ever lands in a private forge repository. */ export async function assertMirrorPullAllowed(row: IntegrationRow, repo: RepoRow): Promise { const config = parseConfig(row); if (!config.githubInstallationId || !config.githubRepo || !isGithubRepoName(config.githubRepo)) throw badRequest("The GitHub mirror is missing an installation or repository"); const user = row.created_by ? await loadUserById(row.created_by) : null; if (!user || !hasPermission(await repoPermission(repo, toViewer(user)), "write")) { throw badRequest("The user who configured this mirror no longer has write access to this repository"); } const token = await installationToken(config.githubInstallationId); const source = await githubApi<{ private?: boolean }>(token, `/repos/${config.githubRepo}`); const githubPrivate = source.private !== false; let installerPermission: string | null = null; if (githubPrivate && config.githubLogin) { const result = await githubApi( token, `/repos/${config.githubRepo}/collaborators/${encodeURIComponent(config.githubLogin)}/permission`, ); installerPermission = result.permission ?? null; } const decision = mirrorPullDecision({ githubPrivate, forgeVisibility: repo.visibility, installerPermission }); if (!decision.ok) throw badRequest(decision.reason); } /** gitfor → GitHub: relays a pack from Artifacts into GitHub's receive-pack using an installation token. */ export async function mirrorToGithub(row: IntegrationRow, repo: RepoRow): Promise { await assertMirrorPushAllowed(row, repo); const remote = await githubRemote(row); const self = await artifactsSource(repo); const [local, upstream] = await Promise.all([advertisement(self), advertisement(remote)]); const { commands, skipped } = planMirror(local, upstream); if (commands.length === 0) return { updated: 0, rejected: [], skipped }; const report = await relayPack({ source: self, target: remote, wants: [...new Set(commands.map((command) => command.sha))], haves: upstream.map((ref) => ref.sha), commands, maxBytes: MAX_MIRROR_BYTES, }); return { updated: report.accepted, rejected: report.rejected, skipped }; } const MAX_TRIGGERED_REFS = 50; function emptyPack(): ReadableStream { return new ReadableStream({ start: (controller) => controller.close() }); } /** receive-pack `ng` entries are ` `. */ export function reportRefName(entry: string): string { const space = entry.indexOf(" "); return space === -1 ? entry : entry.slice(0, space); } const MAX_MIRROR_PASSES = 5; type MirrorApply = { accepted: string[]; rejected: string[] }; async function applyMirror( repo: RepoRow, remote: RelaySource, local: AdvertisedRef[], commands: RefCommand[], policy: MirrorPolicy, ): Promise { const present = new Set(local.map((ref) => ref.sha)); const wants = [...new Set(commands.map((command) => command.sha))].filter((sha) => !present.has(sha)); const screen = (body: ReadableStream) => screenMirrorPush(repo, commands, body, policy); if (wants.length === 0) { const screened = await screenMirrorPush( repo, commands, receivePackBody(commands, emptyPack()), policy, undefined, false, ); await screened.body.cancel().catch(() => undefined); if (screened.rejected.size > 0) throw new RelayScreenRejected(screened.rejected); await pushToArtifacts( repo, commands.map((command) => ({ ref: command.name, oldSha: command.oldSha ?? ZERO_SHA, newSha: command.sha })), [], ); return { accepted: commands.map((command) => command.name), rejected: [] }; } const report = await relayPack({ source: remote, target: repo, wants, haves: [...present], commands, maxBytes: MAX_MIRROR_BYTES, screen, }); const failed = new Set(report.rejected.map(reportRefName)); return { accepted: commands.map((command) => command.name).filter((name) => !failed.has(name)), rejected: report.rejected, }; } /** * GitHub → gitfor. Every update goes through the same gates as a git push (hidden refs, branch protection, * fast-forward-only unless force mirroring is enabled, push protection), refused refs are dropped and the rest * retried once, and applied refs fire normal push events so CI, webhooks and integrations run. */ export async function mirrorFromGithub(row: IntegrationRow, repo: RepoRow): Promise { await assertMirrorPullAllowed(row, repo); const config = parseConfig(row); const policy: MirrorPolicy = { force: config.githubForceMirror === true, pusherId: row.created_by }; const remote = await githubRemote(row); const self = await artifactsSource(repo); const [upstream, local] = await Promise.all([advertisement(remote), advertisement(self)]); const planned = planMirror(upstream, local); const visible = withoutHiddenRefs(planned.commands); const refused = new Map(visible.rejected); let commands = visible.commands; let applied: MirrorApply = { accepted: [], rejected: [] }; let done = false; for (let pass = 0; pass < MAX_MIRROR_PASSES && commands.length > 0 && !done; pass++) { try { applied = await applyMirror(repo, remote, local, commands, policy); done = true; } catch (error) { if (!(error instanceof RelayScreenRejected)) throw error; for (const [ref, reason] of error.rejected) refused.set(ref, reason); commands = commands.filter((command) => !error.rejected.has(command.name)); } } if (!done) for (const command of commands) refused.set(command.name, "not applied after retries; will retry"); const byName = new Map(commands.map((command) => [command.name, command])); for (const name of applied.accepted.slice(0, MAX_TRIGGERED_REFS)) { const command = byName.get(name); if (!command) continue; await triggerPush({ source: "internal", repoId: repo.id, artifactsName: repo.artifacts_name, ref: command.name, before: command.oldSha ?? ZERO_SHA, after: command.sha, pusherId: row.created_by, }); } for (const entry of applied.rejected) refused.set(reportRefName(entry), entry.slice(reportRefName(entry).length).trim() || "rejected by the repository"); return { updated: applied.accepted.length, rejected: [...refused].map(([ref, reason]) => `${ref}: ${reason}`), skipped: planned.skipped, }; } /** GitHub integrations react to pushes (mirror out) and `mirror.pull` ticks / relayed GitHub pushes (mirror in). */ export async function handleGithubEvent(row: IntegrationRow, input: DeliverInput): Promise { const ok: OutboundResult = { status: 200, body: "", error: null, durationMs: 0 }; const config = parseConfig(row); if (input.event === "ping") { if (!config.githubInstallationId) return { ...ok, status: null, error: "No GitHub App installation", permanent: true }; await installationToken(config.githubInstallationId); return { ...ok, body: "Installation token issued" }; } const direction = config.githubMirror ?? "none"; const wantsPush = direction === "push" && (input.event === "push" || input.event === "mirror.push"); const wantsPull = direction === "pull" && (input.event === "mirror.pull" || input.event === "github.push"); if ((!wantsPush && !wantsPull) || !input.repoId) return ok; const repo = await loadRepoById(input.repoId); if (!repo) return ok; const started = Date.now(); const result = wantsPush ? await mirrorToGithub(row, repo) : await mirrorFromGithub(row, repo); return { status: 200, body: JSON.stringify(result), error: result.rejected.length > 0 && (result.updated === 0 || !result.rejected.every(isPolicyRefusal)) ? `Refused: ${result.rejected.slice(0, 5).join("; ")}` : null, permanent: result.rejected.length > 0 && result.rejected.every(isPolicyRefusal), durationMs: Date.now() - started, }; }