// --------------------------------------------------------------------------- // OAuth popup HTTP helpers — server-side. // // `popupDocument` renders the HTML page returned by the OAuth redirect // handler. The page immediately `postMessage`s the result back to the // opener window and falls back to a `BroadcastChannel` if the opener is // gone (mobile Safari closes the opener on popup open in some cases). // // `runOAuthCallback` wraps the "call completeOAuth, turn Exit into // popup payload, render HTML" glue so plugin handlers stay one-liners. // --------------------------------------------------------------------------- import { Cause, Effect } from "effect"; import { decodeOAuthCallbackState, OAUTH_POPUP_MESSAGE_TYPE, type OAuthPopupResult, } from "@executor-js/sdk"; export { OAUTH_POPUP_MESSAGE_TYPE, isOAuthPopupResult } from "@executor-js/sdk"; export type { OAuthPopupResult } from "@executor-js/sdk"; // --------------------------------------------------------------------------- // Completion listener — optional process-wide hook called every time an // OAuth flow finishes (success or failure). Lets hosts that can't use the // in-browser postMessage/BroadcastChannel handoff observe results another // way (e.g. the local server registers an in-memory registry so the // Electron renderer can poll over HTTP when the user signed in via the // system browser). // // Default: no listener. Cloud hosts (Cloudflare Workers — stateless, // multi-isolate) intentionally don't register one; an in-memory side // channel can't bridge isolates and the same-origin web SPA already // receives results via postMessage, so the listener is a no-op there. // --------------------------------------------------------------------------- export type OAuthCompletionListener = (result: OAuthPopupResult) => void; // TODO: replace with plugin notify framework let completionListener: OAuthCompletionListener | null = null; export const setOAuthCompletionListener = (listener: OAuthCompletionListener | null): void => { completionListener = listener; }; // --------------------------------------------------------------------------- // HTML generation // --------------------------------------------------------------------------- const escapeHtml = (value: string): string => value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); /** * Serialize for embedding inside a ` `; }; // --------------------------------------------------------------------------- // Callback wrapper — turns a completeOAuth Effect into a popup HTML string. // --------------------------------------------------------------------------- export type OAuthCallbackUrlParams = { readonly state: string; readonly code?: string | null; readonly error?: string | null; readonly error_description?: string | null; /** Non-standard regional host hints (Datadog: `domain` bare host, `site` * full origin) used to redeem the code at the org's region. */ readonly domain?: string | null; readonly site?: string | null; }; /** Short summary + optional full technical detail. */ export type PopupErrorMessage = { readonly short: string; readonly details?: string; }; export type RunOAuthCallbackInput = { /** The plugin's `completeOAuth` — resolves to the auth descriptor on success. */ readonly complete: (params: { readonly state: string; readonly code: string | null; readonly error: string | null; readonly callbackDomain: string | null; }) => Effect.Effect; readonly urlParams: OAuthCallbackUrlParams; /** Map a plugin-specific error into a short summary and optional details. */ readonly toErrorMessage: (error: unknown) => PopupErrorMessage; readonly channelName: string; }; const providerErrorMessage = (params: OAuthCallbackUrlParams): PopupErrorMessage | null => { const error = params.error ?? null; const description = params.error_description ?? null; const value = error ?? description; if (!value) return null; return { short: "OAuth provider rejected authorization", details: error && description && description !== error ? `${error}: ${description}` : value, }; }; /** * Run a plugin's `completeOAuth` against URL params from the OAuth redirect, * wrap the success / failure in an `OAuthPopupResult`, and return the HTML * body ready to hand to `HttpServerResponse.html(...)`. * * This never fails — errors become a `{ ok: false }` result so the popup * can still render and close itself. */ export const runOAuthCallback = ( input: RunOAuthCallbackInput, ): Effect.Effect => { const providerError = providerErrorMessage(input.urlParams); const callbackState = decodeOAuthCallbackState(input.urlParams.state); const sessionId = callbackState?.state ?? input.urlParams.state; const result = providerError == null ? input .complete({ state: sessionId, code: input.urlParams.code ?? null, error: null, callbackDomain: input.urlParams.domain ?? input.urlParams.site ?? null, }) .pipe( Effect.map( (auth): OAuthPopupResult => ({ type: OAUTH_POPUP_MESSAGE_TYPE, ok: true, sessionId, ...auth, }), ), ) : Effect.succeed>({ type: OAUTH_POPUP_MESSAGE_TYPE, ok: false, sessionId, error: providerError.short, ...(providerError.details ? { errorDetails: providerError.details } : {}), }); return result.pipe( Effect.catchCause((cause) => { const { short, details } = input.toErrorMessage(Cause.squash(cause)); return Effect.succeed>({ type: OAUTH_POPUP_MESSAGE_TYPE, ok: false, sessionId, error: short, ...(details && details !== short ? { errorDetails: details } : {}), }); }), Effect.tap((result) => Effect.sync(() => completionListener?.(result as OAuthPopupResult)), ), Effect.map((result) => popupDocument(result, input.channelName)), ); };