514 lines · 19.2 KB · TypeScript
RawDownload
import { useCallback, useEffect, useRef, useState } from "react";import { useAtomSet } from "@effect/atom-react";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit"; import { cancelOAuth, oauthConnectionCompleted, startOAuth } from "../api/atoms";import { trackEvent } from "../api/analytics";import { messageFromExit, messageFromUnknown, useReportHandledError } from "../api/error-reporting";import {  openOAuthPopup,  openOAuthSystemBrowser,  reserveOAuthPopup,  type OAuthPopupResult,  type ReservedOAuthPopup,} from "../api/oauth-popup";import { connectionWriteKeys } from "../api/reactivity-keys";import { getActiveOrgSlug } from "../api/server-connection"; export type DesktopBridge = {  readonly openExternal: (url: string) => Promise<void>;}; const getDesktopBridge = (): DesktopBridge | null => {  if (typeof window === "undefined") return null;  const candidate = (window as { readonly executor?: Partial<DesktopBridge> }).executor;  return candidate && typeof candidate.openExternal === "function"    ? // oxlint-disable-next-line executor/no-double-cast -- boundary: narrowed by the typeof guard above      (candidate as DesktopBridge)    : null;};import { Button } from "../components/button";import {  OAUTH_POPUP_MESSAGE_TYPE,  OAuthState,  type AuthTemplateSlug,  type ConnectionName,  type IntegrationSlug,  type OAuthClientSlug,  type Owner,} from "@executor-js/sdk/shared"; // ---------------------------------------------------------------------------// OAuth sign-in (v2). A registered OAuth client (`oauth.createClient`) is run by// `oauth.start` for one integration, minting an owner-scoped Connection. `start`// either returns the connection inline (`status: "connected"`) or a redirect// (`status: "redirect"` with an `authorizationUrl` + `state`); the popup// completes the redirect via the server `/oauth/callback`, then posts the result// back. `state` is the correlation token (the v1 session id).//// NOTE(v2): the server-side flow is stubbed (D18). The shapes below track the// v2 contract so this UI compiles; the plugin `/react` wave wires the buttons.// --------------------------------------------------------------------------- export type OAuthCompletionPayload = {  readonly owner: Owner;  readonly integration: IntegrationSlug;  readonly name: ConnectionName;  readonly identityLabel?: string | null;}; export type OAuthStartPayload = {  /** Registered OAuth client slug to run. */  readonly client: OAuthClientSlug;  /** Owner of `client` (a Personal connection may use a shared Workspace app). */  readonly clientOwner: Owner;  readonly owner: Owner;  /** Name for the connection the flow mints. */  readonly name: ConnectionName;  readonly integration: IntegrationSlug;  readonly template: AuthTemplateSlug;  readonly identityLabel?: string;  /** Mint a NEW connection: the server resolves a taken `name` to the next   *  free suffixed one. Omitted on reconnect, which targets the existing row. */  readonly newConnection?: boolean;  readonly redirectUri?: string;}; export type StartOAuthPopupInput<TPayload extends OAuthCompletionPayload> = {  readonly payload: OAuthStartPayload;  readonly onSuccess: (payload: TPayload) => void | Promise<void>;  readonly onError?: (error: string) => void;  readonly onAuthorizationStarted?: (result: OAuthAuthorizationStartResult) => void;  /** Window reserved on the click that began this flow. See `reserve`. */  readonly reservation?: OAuthPopupReservation;}; /** * A sign-in window claimed synchronously inside a click handler. * * `window.open` requires transient user activation, which Chromium expires * about five seconds after the click. Any flow that awaits the network before * it has an authorization URL (DCR probes then registers, CIMD creates a * client, reconnect starts a session) therefore cannot open the window when the * URL finally arrives: by then the activation is gone and the browser refuses * silently. Those flows reserve a blank window on the click and navigate it * later, however long the round trips take. * * Desktop hosts hand the URL to the real browser, so they reserve nothing and * carry the bridge that will open it instead. */export type OAuthPopupReservation =  | { readonly kind: "desktop"; readonly bridge: DesktopBridge }  | { readonly kind: "window"; readonly popup: ReservedOAuthPopup }  | { readonly kind: "blocked" }; const POPUP_BLOCKED_MESSAGE = "Sign-in popup was blocked by the browser"; export type OAuthAuthorizationStartResult = {  /** OAuth correlation token (was the v1 session id). */  readonly state: string;  readonly authorizationUrl: string | null;}; class OAuthAuthorizationStartError extends Data.TaggedError("OAuthAuthorizationStartError")<{  readonly cause: unknown;  readonly message: string;}> {} export type StartOAuthAuthorizationInput<TPayload extends OAuthCompletionPayload> = {  readonly owner: Owner;  readonly run: () => Promise<OAuthAuthorizationStartResult>;  readonly onSuccess: (payload: TPayload) => void | Promise<void>;  readonly onError?: (error: string, details?: string) => void;  readonly onAuthorizationStarted?: (result: OAuthAuthorizationStartResult) => void;  readonly reportMetadata?: Record<string, string | number | boolean | null | undefined>;  /** Window reserved on the click that began this flow. See `reserve`. */  readonly reservation?: OAuthPopupReservation;}; export function oauthCallbackUrl(path = "/api/oauth/callback"): string {  if (typeof window === "undefined") return path;  return new URL(path, window.location.origin).toString();} export const OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH = "/api/oauth/client-id-metadata/default.json";export const OAUTH_CLIENT_ID_METADATA_DOCUMENT_LOCAL_PATH =  "/api/oauth/client-id-metadata/local.json"; const configuredClientIdMetadataBaseUrl = (): string | undefined => {  const env = (    import.meta as ImportMeta & {      readonly env?: Record<string, string | undefined>;    }  ).env;  const value = env?.VITE_EXECUTOR_CIMD_CLIENT_ID_METADATA_BASE_URL?.trim();  return value ? value : undefined;}; export function oauthClientIdMetadataDocumentUrl(options?: {  readonly hostedBaseUrl?: string | null;  readonly path?: string;}): string {  const hostedBaseUrl = options?.hostedBaseUrl ?? configuredClientIdMetadataBaseUrl();  if (hostedBaseUrl) {    return new URL(OAUTH_CLIENT_ID_METADATA_DOCUMENT_LOCAL_PATH, hostedBaseUrl).toString();  }   const defaultPath = options?.path ?? OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH;  if (typeof window === "undefined") return defaultPath;  const orgSlug = getActiveOrgSlug();  const metadataPath = orgSlug    ? `/api/oauth/client-id-metadata/${encodeURIComponent(orgSlug)}.json`    : defaultPath;  const url = new URL(metadataPath, window.location.origin);  return url.toString();} export function useOAuthPopupFlow<  TPayload extends OAuthCompletionPayload = OAuthCompletionPayload,>(options: {  readonly popupName: string;  readonly callbackPath?: string;  readonly noAuthorizationUrlMessage?: string;  readonly popupBlockedMessage?: string;  readonly popupClosedMessage?: string;  readonly detectPopupClosed?: boolean;  readonly startErrorMessage?: string;}) {  const {    detectPopupClosed = true,    callbackPath,    noAuthorizationUrlMessage,    popupBlockedMessage,    popupClosedMessage,    popupName,    startErrorMessage,  } = options;  const doStartOAuth = useAtomSet(startOAuth, { mode: "promiseExit" });  const doCancelOAuth = useAtomSet(cancelOAuth, { mode: "promiseExit" });  const doOAuthConnectionCompleted = useAtomSet(oauthConnectionCompleted, { mode: "promiseExit" });  const reportHandledError = useReportHandledError();  const blockedMessage = popupBlockedMessage ?? POPUP_BLOCKED_MESSAGE;  const [busy, setBusy] = useState(false);  const [error, setError] = useState<string | null>(null);  const cleanupRef = useRef<(() => void) | null>(null);  const sessionRef = useRef<{ readonly state: string } | null>(null);  // A window reserved on the click but not yet handed to a flow owns nothing  // that would close it: `cleanupRef` is only set once the authorization URL  // exists. Hold it here so cancel and unmount can close it, or abandoning a  // connect mid-probe strands a blank popup on screen.  const reservationRef = useRef<ReservedOAuthPopup | null>(null);   const releaseReservation = useCallback(() => {    reservationRef.current?.popup.close();    reservationRef.current = null;  }, []);   const cancelSession = useCallback(    (state: string) => {      void doCancelOAuth({ payload: { state: OAuthState.make(state) } });    },    [doCancelOAuth],  );   const cancel = useCallback(() => {    const session = sessionRef.current;    cleanupRef.current?.();    cleanupRef.current = null;    sessionRef.current = null;    releaseReservation();    if (session) cancelSession(session.state);    setBusy(false);  }, [cancelSession, releaseReservation]);   useEffect(    () => () => {      const session = sessionRef.current;      cleanupRef.current?.();      cleanupRef.current = null;      sessionRef.current = null;      reservationRef.current?.popup.close();      reservationRef.current = null;      if (session) cancelSession(session.state);    },    [cancelSession],  );   /**   * Claim the sign-in window NOW, while the click still carries user   * activation, and return what was claimed so the caller can abandon a flow   * the browser already refused instead of running a doomed authorization.   *   * Callers that reach `openAuthorization` synchronously do not need this; it   * reserves for itself.   */  const reserve = useCallback((): OAuthPopupReservation => {    // Tear the previous flow down BEFORE opening: `window.open` reuses a window    // already carrying `popupName`, so cancelling afterwards would close the    // very window just reserved.    cancel();    const bridge = getDesktopBridge();    if (bridge !== null) return { kind: "desktop", bridge };    const popup = reserveOAuthPopup({ popupName });    if (popup === null) {      trackEvent("oauth_popup_blocked");      setError(blockedMessage);      return { kind: "blocked" };    }    reservationRef.current = popup;    return { kind: "window", popup };  }, [blockedMessage, cancel, popupName]);   const openAuthorization = useCallback(    async (input: StartOAuthAuthorizationInput<TPayload>) => {      // A caller that reserved on its own click already ran `cancel` inside      // `reserve`; cancelling again here would close the window it reserved.      const reservation = input.reservation ?? reserve();      if (reservation.kind === "blocked") {        setBusy(false);        setError(blockedMessage);        input.onError?.(blockedMessage);        return;      }      setBusy(true);      setError(null);      // Desktop hosts open the auth URL in the user's real browser, so they      // reserve no in-page window and rely on the polling channel for the      // result.      const desktopBridge = reservation.kind === "desktop" ? reservation.bridge : null;      const reservedPopup = reservation.kind === "window" ? reservation.popup : null;      // The window's lifetime now belongs to this flow's teardown, which closes      // it on every failure path below.      reservationRef.current = null;      const startExit = await Effect.runPromiseExit(        Effect.tryPromise({          try: input.run,          catch: (cause) =>            new OAuthAuthorizationStartError({              cause,              message: messageFromUnknown(cause, startErrorMessage ?? "Failed to start sign-in"),            }),        }),      );      if (Exit.isFailure(startExit)) {        const message = messageFromExit(startExit, startErrorMessage ?? "Failed to start sign-in");        reportHandledError(startExit.cause, {          surface: "oauth",          action: "start",          message,          metadata: input.reportMetadata,        });        reservedPopup?.popup.close();        setBusy(false);        setError(message);        input.onError?.(message);        return;      }      const response = startExit.value;      if (response.authorizationUrl === null) {        const message =          noAuthorizationUrlMessage ?? "OAuth start did not produce an authorization URL";        reservedPopup?.popup.close();        setBusy(false);        setError(message);        input.onError?.(message);        return;      }       sessionRef.current = { state: response.state };      input.onAuthorizationStarted?.(response);      const handleResult = async (result: OAuthPopupResult<TPayload>) => {        cleanupRef.current = null;        sessionRef.current = null;         if (!result.ok) {          trackEvent("oauth_completed", { success: false });          setBusy(false);          setError(result.error);          input.onError?.(result.error, result.errorDetails);          return;        }         const refreshExit = await doOAuthConnectionCompleted({          reactivityKeys: connectionWriteKeys,        });        if (Exit.isFailure(refreshExit)) {          const message = messageFromExit(refreshExit, "Failed to refresh connection");          reportHandledError(refreshExit.cause, {            surface: "oauth",            action: "refresh_connection",            message,            metadata: input.reportMetadata,          });          trackEvent("oauth_completed", { success: false });          setBusy(false);          setError(message);          input.onError?.(message);          return;        }         const persistenceError = await Promise.resolve(input.onSuccess(result)).then(          () => null,          (cause: unknown) => cause,        );        if (persistenceError !== null) {          const message = messageFromUnknown(persistenceError, "Failed to save connection");          reportHandledError(persistenceError, {            surface: "oauth",            action: "persist_connection",            message,            metadata: input.reportMetadata,          });          trackEvent("oauth_completed", { success: false });          setBusy(false);          setError(message);          input.onError?.(message);          return;        }        trackEvent("oauth_completed", { success: true });        setBusy(false);      };      const handleClosed = () => {        cleanupRef.current = null;        sessionRef.current = null;        // `popup.closed` is advisory: COOP redirects can make a live popup        // appear closed to the opener. Keep server OAuth state alive for a        // callback or TTL cleanup; only explicit cancel deletes the session.        const message =          popupClosedMessage ?? "Sign-in cancelled - popup was closed before completing the flow.";        trackEvent("oauth_completed", { success: false });        setBusy(false);        setError(message);        input.onError?.(message);      };      const handleOpenFailed = () => {        cleanupRef.current = null;        sessionRef.current = null;        cancelSession(response.state);        trackEvent("oauth_completed", { success: false });        setBusy(false);        setError(blockedMessage);        input.onError?.(blockedMessage);      };       cleanupRef.current = desktopBridge        ? openOAuthSystemBrowser<TPayload>({            url: response.authorizationUrl,            sessionId: response.state,            openExternal: desktopBridge.openExternal,            onResult: (result) => void handleResult(result),            onOpenFailed: handleOpenFailed,            onTimeout: handleClosed,          })        : openOAuthPopup<TPayload>({            url: response.authorizationUrl,            popupName,            channelName: OAUTH_POPUP_MESSAGE_TYPE,            expectedSessionId: response.state,            reservedPopup: reservedPopup ?? undefined,            closedPollMs: detectPopupClosed ? undefined : null,            onResult: (result) => void handleResult(result),            onClosed: handleClosed,            onOpenFailed: handleOpenFailed,          });    },    [      blockedMessage,      cancelSession,      detectPopupClosed,      doOAuthConnectionCompleted,      noAuthorizationUrlMessage,      popupClosedMessage,      popupName,      reportHandledError,      reserve,      startErrorMessage,    ],  );   const start = useCallback(    async (input: StartOAuthPopupInput<TPayload>) => {      await openAuthorization({        owner: input.payload.owner,        onSuccess: input.onSuccess,        onError: input.onError,        onAuthorizationStarted: input.onAuthorizationStarted,        ...(input.reservation === undefined ? {} : { reservation: input.reservation }),        reportMetadata: {          client: String(input.payload.client),          integration: String(input.payload.integration),          name: String(input.payload.name),          owner: input.payload.owner,        },        run: () =>          doStartOAuth({            payload: {              client: input.payload.client,              clientOwner: input.payload.clientOwner,              owner: input.payload.owner,              name: input.payload.name,              integration: input.payload.integration,              template: input.payload.template,              identityLabel: input.payload.identityLabel,              newConnection: input.payload.newConnection,              redirectUri: input.payload.redirectUri ?? oauthCallbackUrl(callbackPath),            },          }).then((exit) =>            Exit.isSuccess(exit)              ? // The redirect branch carries `authorizationUrl` + `state`; the                // inline "connected" (client_credentials) branch has no URL to                // open and no redirect, so `state` is intentionally empty — it                // is never read for an already-minted connection.                exit.value.status === "redirect"                ? { state: exit.value.state, authorizationUrl: exit.value.authorizationUrl }                : { state: "", authorizationUrl: null }              : Effect.runPromise(                  Effect.fail({                    message: messageFromExit(exit, startErrorMessage ?? "Failed to start sign-in"),                  }),                ),          ),      });    },    [callbackPath, doStartOAuth, openAuthorization, startErrorMessage],  );   return {    busy,    error,    setError,    start,    openAuthorization,    cancel,    reserve,    releaseReservation,  };} export function OAuthSignInButton(props: {  readonly busy: boolean;  readonly error: string | null;  readonly isConnected: boolean;  readonly onSignIn: () => void;  readonly reconnectingLabel?: string;  readonly signingInLabel?: string;  readonly reconnectLabel?: string;  readonly signInLabel?: string;}) {  return (

Lines 1–500 of 514.