170 lines · 6.6 KB · TypeScript
RawDownload
import { createServer, type Server, type ServerResponse } from "node:http";import { Effect, Fiber } from "effect";import {  beginGoogleAuthorization,  cancelGoogleAuthorization,  completeGoogleAuthorizationWithActivation,  createGoogleAuthorizationFlow,  GoogleAccountError,} from "./google-account";import {  enableGoogleWorkspaceAdapter,  googleWorkspaceAdapterEnabled,  restoreGoogleWorkspaceAdapter,} from "./google-workspace-adapter";import type { GoogleWorkspacePluginId } from "./google-workspace-binding";import { createOAuthLoopbackLifecycle } from "./oauth-loopback-lifecycle"; type ActiveFlow = {  id: string;  server: Server;  timeout: Fiber.Fiber<void, unknown>;}; const activeFlows = new Map<GoogleWorkspacePluginId, ActiveFlow>();const loopbackLifecycles = {  gmail: createOAuthLoopbackLifecycle(),  "google-calendar": createOAuthLoopbackLifecycle(),}; function page(response: ServerResponse, success: boolean, activated: boolean): Promise<void> {  const title = success ? "Google Workspace connected" : "Google sign-in failed";  const status = success ? "Connection complete" : "Action needed";  const message = !success    ? "Return to Local Studio and start Google sign-in again."    : activated      ? "The read-only plugin is ready in Local Studio. You can close this tab."      : "The account is connected. Return to Local Studio to finish enabling the plugin.";  const role = success ? "status" : "alert";  const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${title}</title><style>:root{color-scheme:light dark;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}*{box-sizing:border-box}body{min-height:100vh;margin:0;display:grid;place-items:center;padding:1.5rem;background:Canvas;color:CanvasText}main{width:min(100%,32rem);padding:2rem;border:1px solid color-mix(in srgb,CanvasText 16%,transparent);border-radius:1rem;background:color-mix(in srgb,Canvas 94%,CanvasText 6%);box-shadow:0 1.5rem 4rem color-mix(in srgb,CanvasText 10%,transparent)}.brand{margin:0 0 2.5rem;font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.status{margin:0 0 .75rem;font-size:.8125rem;font-weight:650;color:color-mix(in srgb,CanvasText 68%,transparent)}h1{margin:0;font-size:clamp(1.5rem,5vw,2rem);line-height:1.15;letter-spacing:-.025em}p:last-child{margin:1rem 0 0;color:color-mix(in srgb,CanvasText 68%,transparent);line-height:1.6}</style></head><body><main><p class="brand">Local Studio</p><p class="status" role="${role}">${status}</p><h1>${title}</h1><p>${message}</p></main></body></html>`;  return new Promise((resolve) => {    let settled = false;    const finish = () => {      if (settled) return;      settled = true;      resolve();    };    response.once("finish", finish);    response.once("close", finish);    response.once("error", finish);    response.writeHead(success ? 200 : 400, {      "content-type": "text/html; charset=utf-8",      "cache-control": "no-store",      connection: "close",    });    response.end(html);  });} function closeFlow(  account: GoogleWorkspacePluginId,  expectedId?: string,  interruptTimeout = true,): void {  const flow = activeFlows.get(account);  if (!flow || (expectedId && flow.id !== expectedId)) return;  activeFlows.delete(account);  flow.server.closeAllConnections();  flow.server.close();  if (interruptTimeout) void Effect.runPromise(Fiber.interrupt(flow.timeout));} async function handleCallback(  account: GoogleWorkspacePluginId,  flowId: string,  requestUrl: string,  response: ServerResponse,): Promise<void> {  const url = new URL(requestUrl, "http://127.0.0.1");  if (url.pathname !== "/callback") {    response.writeHead(404).end();    return;  }  const state = url.searchParams.get("state");  const code = url.searchParams.get("code");  if (url.searchParams.has("error") || !state || !code) {    await page(response, false, false);    closeFlow(account, flowId);    return;  }  try {    const adapterWasEnabled = await Effect.runPromise(googleWorkspaceAdapterEnabled(account));    const result = await Effect.runPromise(      completeGoogleAuthorizationWithActivation(        account,        { state, code },        flowId,        (signal) =>          enableGoogleWorkspaceAdapter(account, signal).pipe(            Effect.as(true),            Effect.catch(() => Effect.succeed(false)),          ),        restoreGoogleWorkspaceAdapter(account, adapterWasEnabled),      ),    );    await page(response, true, result.activation);    closeFlow(account, flowId);  } catch (error) {    await page(response, false, false);    if (!(error instanceof GoogleAccountError && error.message.includes("state is invalid"))) {      closeFlow(account, flowId);    }  }} function listen(server: Server): Promise<number> {  return new Promise((resolve, reject) => {    server.once("error", reject);    server.listen(0, "127.0.0.1", () => {      server.removeListener("error", reject);      const address = server.address();      if (!address || typeof address === "string") {        reject(new Error("Loopback listener failed"));        return;      }      resolve(address.port);    });  });} export function beginGoogleLoopbackAuthorization(  account: GoogleWorkspacePluginId,): Effect.Effect<{ authorizationUrl: string }, GoogleAccountError> {  return loopbackLifecycles[account].start(    Effect.gen(function* () {      closeFlow(account);      const flowId = createGoogleAuthorizationFlow(account);      const server = createServer((request, response) => {        void handleCallback(account, flowId, request.url ?? "/", response);      });      const port = yield* Effect.tryPromise({        try: () => listen(server),        catch: () => new GoogleAccountError(500, "Could not start the private OAuth callback"),      });      server.on("error", () => closeFlow(account, flowId));      const timeout = Effect.runFork(        Effect.gen(function* () {          yield* Effect.sleep(10 * 60 * 1000);          closeFlow(account, flowId, false);        }),      );      activeFlows.set(account, { id: flowId, server, timeout });      return yield* beginGoogleAuthorization(        account,        `http://127.0.0.1:${port}/callback`,        undefined,        undefined,        flowId,      ).pipe(Effect.tapError(() => Effect.sync(() => closeFlow(account, flowId))));    }),  );} export function cancelGoogleLoopbackAuthorization(  account: GoogleWorkspacePluginId,): Effect.Effect<void, GoogleAccountError> {  return loopbackLifecycles[account].cancel(    cancelGoogleAuthorization(account),    Effect.sync(() => closeFlow(account)),  );}