import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { limitHeavy, limitWrite } from "~/server/limits"; import { assertVerifiedForWrites } from "~/server/verified-writes"; import { assertNotArchived, requireRepoAccess } from "~/server/repos"; import { badRequest } from "~/server/errors"; import { advertisement, assertPublicHttpsUrl } from "~/server/git/relay"; import { assertResolvesPublic } from "~/server/net/resolve-guard"; import { sourceAuthorization } from "~/server/git/ref-sync"; import { publicUrl } from "~/server/events/emit"; import { assertMirrorAllowed } from "~/server/mirror/mirror-do"; import { configureMirror, mirrorStatus, removeMirror, requestMirrorSync, rotateMirrorWebhookSecret, } from "~/server/mirror/client"; import { CONFLICT_STRATEGIES, MIRROR_DIRECTIONS } from "~/lib/mirror"; import { repoRef } from "~/lib/validation"; const MAX_TOKEN = 1000; const MAX_PATTERNS = 50; function hookUrl(repoId: string): string { return publicUrl(`/integrations/hooks/mirror/${repoId}`); } export const getMirror = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "admin"); return { status: await mirrorStatus(repo.id), webhookUrl: hookUrl(repo.id) }; }); const saveInput = repoRef.extend({ remoteUrl: z.string().trim().min(1).max(500), username: z.string().trim().max(100).nullable(), /** Omitted keeps the stored token; null clears it. */ token: z.string().trim().max(MAX_TOKEN).nullable().optional(), direction: z.enum(MIRROR_DIRECTIONS as [string, ...string[]]), strategy: z.enum(CONFLICT_STRATEGIES as [string, ...string[]]), branches: z.array(z.string().trim().min(1).max(200)).max(MAX_PATTERNS), syncTags: z.boolean(), propagateDeletes: z.boolean(), enabled: z.boolean(), }); export const saveMirror = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(saveInput) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); assertVerifiedForWrites(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); assertNotArchived(repo); if (repo.status !== "ready") throw badRequest("The repository is still being provisioned"); await limitHeavy(viewer); const remoteUrl = assertPublicHttpsUrl(data.remoteUrl).toString().replace(/\/$/, ""); await assertResolvesPublic(remoteUrl); const current = await mirrorStatus(repo.id); const keepsToken = data.token === undefined || data.token === ""; const remoteChanged = current !== null && current.remoteUrl !== remoteUrl; if (keepsToken && remoteChanged && current.hasToken) { throw badRequest("Enter the access token again when changing the remote URL"); } const token = keepsToken ? undefined : data.token; const hasToken = token === undefined ? Boolean(current?.hasToken) : token !== null; const settings = { remoteUrl, username: data.username || null, direction: data.direction as (typeof MIRROR_DIRECTIONS)[number], strategy: data.strategy as (typeof CONFLICT_STRATEGIES)[number], branches: data.branches, syncTags: data.syncTags, propagateDeletes: data.propagateDeletes, createdBy: viewer.id, }; if (data.enabled && typeof token === "string") { await advertisement({ url: remoteUrl, authorization: sourceAuthorization(token, settings.username || "git") }); } if (data.enabled) await assertMirrorAllowed(repo, settings, hasToken); const { createdBy, ...input } = settings; return configureMirror(repo.id, createdBy, { ...input, enabled: data.enabled }, token); }); export const deleteMirror = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); await limitWrite(viewer); await removeMirror(repo.id); return { ok: true }; }); export const syncMirrorNow = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); assertVerifiedForWrites(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); assertNotArchived(repo); await limitHeavy(viewer); const result = await requestMirrorSync(repo.id, "manual"); if (!result.ok) throw badRequest(result.reason); return { at: result.at }; }); export const rotateMirrorSecret = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); await limitWrite(viewer); const secret = await rotateMirrorWebhookSecret(repo.id); if (!secret) throw badRequest("Mirroring is not configured"); return { webhookSecret: secret }; });