import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { viewerMiddleware, requireViewer } from "./middleware"; import { limitWrite } from "~/server/limits"; import { unauthorized } from "~/server/errors"; import { createWebhookIntegration, deleteIntegrationById, listIntegrationsFor, redeliverDelivery, sendTestEvent, startGithubJob, updateIntegrationSettings, } from "~/server/integrations/manage"; import { INTEGRATION_EVENTS } from "~/lib/integrations"; const scopeRef = z.object({ owner: z.string().min(1).max(100), repo: z.string().min(1).max(100).nullable() }); const integrationEvent = z.enum(INTEGRATION_EVENTS); const integrationId = z.string().regex(/^[a-z0-9]{20}$/); const MAX_DIGEST_MINUTES = 7 * 24 * 60; async function writer[0] | null }>(context: T) { if (!context.viewer) throw unauthorized("You must be signed in"); await limitWrite(context.viewer); return context.viewer; } export const listIntegrations = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(scopeRef) .handler(async ({ data, context }) => listIntegrationsFor(data, context.viewer)); export const createIntegration = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( scopeRef.extend({ kind: z.enum(["slack", "google_chat", "webhook"]), name: z.string().trim().min(1).max(100), url: z.string().trim().url().max(2000), events: z.array(integrationEvent).max(INTEGRATION_EVENTS.length), secret: z.string().trim().min(8).max(200).nullable(), }), ) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const viewer = await writer(context); return createWebhookIntegration(data, viewer, data); }); export const updateIntegration = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( scopeRef.extend({ id: integrationId, name: z.string().trim().min(1).max(100), active: z.boolean(), events: z.array(integrationEvent).max(INTEGRATION_EVENTS.length), deliveryMode: z.enum(["instant", "digest"]), digestMinutes: z.number().int().min(5).max(MAX_DIGEST_MINUTES), filters: z.object({ branches: z.array(z.string().trim().min(1).max(200)).max(20), ciFailuresOnly: z.boolean(), skipDrafts: z.boolean(), skipBots: z.boolean(), }), slackChannels: z .array(z.object({ channel: z.string().regex(/^[A-Z0-9]{2,30}$/), events: z.array(integrationEvent) })) .max(20) .optional(), linearMapping: z .object({ opened: z.string().max(100), merged: z.string().max(100), closed: z.string().max(100), }) .optional(), githubRepo: z.string().trim().max(200).optional(), githubMirror: z.enum(["none", "push", "pull"]).optional(), githubRelayWebhooks: z.boolean().optional(), githubForceMirror: z.boolean().optional(), }), ) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const viewer = await writer(context); await updateIntegrationSettings(data, viewer, data); return { ok: true }; }); export const deleteIntegration = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(scopeRef.extend({ id: integrationId })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const viewer = await writer(context); await deleteIntegrationById(data, viewer, data.id); return { ok: true }; }); export const testIntegration = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(scopeRef.extend({ id: integrationId })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const viewer = await writer(context); return { deliveryId: await sendTestEvent(data, viewer, data.id) }; }); export const redeliverIntegration = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(scopeRef.extend({ id: integrationId, deliveryId: integrationId })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const viewer = await writer(context); await redeliverDelivery(data, viewer, data.id, data.deliveryId); return { ok: true }; }); export const runGithubJob = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(scopeRef.extend({ id: integrationId, job: z.enum(["import", "mirror"]) })) .handler(async ({ data, context }) => { await limitWrite(requireViewer(context.viewer)); const viewer = await writer(context); await startGithubJob(data, viewer, data.id, data.job); return { ok: true }; });