324 lines · 13.8 KB
import { env } from "cloudflare:workers";
import { z } from "zod";
import { cookieAttributes, cookieName, cookieValue, viewerFromSessionCookie } from "../auth";
import { randomToken, timingSafeEqualStrings } from "../crypto";
import { DEFAULT_INTEGRATION_EVENTS, DEFAULT_FILTERS, DEFAULT_LINEAR_MAPPING } from "~/lib/integrations";
import { isSuccess, outboundFetch } from "./outbound";
import { linearGraphql } from "./linear";
import { requireIntegrationScope } from "./scope";
import type { ResolvedScope } from "./scope";
import { insertIntegration } from "./store";
import { publicUrl } from "../events/emit";

const STATE_TTL_SECONDS = 600;
const STATE_COOKIE = "gitfor_integration_oauth";

type Provider = "slack" | "linear" | "github";
type PendingInstall = { provider: Provider; viewerId: string; scope: ResolvedScope };

function callbackUrl(provider: Provider): string {
  return publicUrl(`/integrations/${provider}/callback`);
}

function redirect(location: string, cookie?: string): Response {
  const headers: Record<string, string> = { location };
  if (cookie) headers["set-cookie"] = cookie;
  return new Response(null, { status: 302, headers });
}

function failure(message: string, status = 400): Response {
  return new Response(`${message}\n`, { status, headers: { "content-type": "text/plain; charset=utf-8" } });
}

/** Starts an install: checks the viewer administers the scope, then binds a one-time state to KV and a cookie. */
async function begin(request: Request, provider: Provider, authorizeUrl: (state: string) => string | null) {
  const url = new URL(request.url);
  const viewer = await viewerFromSessionCookie(request);
  const scope = await requireIntegrationScope(
    { owner: url.searchParams.get("owner") ?? "", repo: url.searchParams.get("repo") || null },
    viewer,
  );