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; }; const activeFlows = new Map(); const loopbackLifecycles = { gmail: createOAuthLoopbackLifecycle(), "google-calendar": createOAuthLoopbackLifecycle(), }; function page(response: ServerResponse, success: boolean, activated: boolean): Promise { 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 = `${title}

Local Studio

${status}

${title}

${message}

`; 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 { 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 { 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 { return loopbackLifecycles[account].cancel( cancelGoogleAuthorization(account), Effect.sync(() => closeFlow(account)), ); }