842 lines · 30.9 KB · TypeScript
RawDownload
import { useEffect, useMemo, useRef, useState } from "react";import { useAtomSet, useAtomValue } from "@effect/atom-react";import * as Exit from "effect/Exit";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms";import { useScope, useScopeStack, useUserScope } from "@executor-js/react/api/scope-context";import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";import { Button } from "@executor-js/react/components/button";import { CopyButton } from "@executor-js/react/components/copy-button";import {  CardStack,  CardStackContent,  CardStackEntry,  CardStackEntryContent,  CardStackEntryDescription,  CardStackEntryField,  CardStackEntryTitle,} from "@executor-js/react/components/card-stack";import { FilterTabs } from "@executor-js/react/components/filter-tabs";import { Input } from "@executor-js/react/components/input";import { sourceWriteKeys as openApiWriteKeys } from "@executor-js/react/api/reactivity-keys";import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk/core";import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";import {  oauthCallbackUrl,  useOAuthPopupFlow,  type OAuthCompletionPayload,} from "@executor-js/react/plugins/oauth-sign-in";import {  effectiveCredentialBindingForScope,  exactCredentialBindingForScope,  isConnectionCredentialBindingValue,  isSecretCredentialBindingValue,} from "@executor-js/react/plugins/credential-bindings";import { SecretCredentialSlotBindings } from "@executor-js/react/plugins/credential-slot-bindings"; import {  openApiSourceAtom,  openApiSourceBindingsAtom,  removeOpenApiSourceBinding,  setOpenApiSourceBinding,  updateOpenApiSource,} from "./atoms";import { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields";import {  OPENAPI_OAUTH_CALLBACK_PATH,  OPENAPI_OAUTH_POPUP_NAME,  inferOAuthIssuerUrl,  resolveOAuthUrl,} from "./AddOpenApiSource";import { oauth2ClientSecretSlot } from "../sdk/store";import {  OAuth2SourceConfig,  OpenApiSourceBindingInput,  type OpenApiSourceBindingRef,} from "../sdk/types"; const ErrorMessage = Schema.Struct({ message: Schema.String });const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string =>  Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {    onNone: () => fallback,    onSome: ({ message }) => message,  }); type SlotDef =  | {      readonly kind: "secret";      readonly slot: string;      readonly label: string;      readonly hint?: string;    }  | {      readonly kind: "oauth2";      readonly slot: string;      readonly label: string;    }; const slugify = (value: string): string =>  value    .trim()    .toLowerCase()    .replace(/[^a-z0-9]+/g, "-")    .replace(/^-+|-+$/g, "") || "default"; const shortHash = (value: string): string => {  let hash = 0x811c9dc5;  for (let i = 0; i < value.length; i++) {    hash ^= value.charCodeAt(i);    hash = Math.imul(hash, 0x01000193);  }  return (hash >>> 0).toString(36).slice(0, 6);}; const openApiOAuthConnectionId = (  sourceId: string,  securitySchemeName: string,  targetScope: ScopeId,): ConnectionId =>  ConnectionId.make(    `openapi-oauth-${slugify(sourceId)}-${slugify(securitySchemeName)}-${shortHash(targetScope)}`,  ); const effectiveClientSecretSlot = (oauth2: {  readonly securitySchemeName: string;  readonly clientSecretSlot: string | null;}): string => oauth2.clientSecretSlot ?? oauth2ClientSecretSlot(oauth2.securitySchemeName); export default function EditOpenApiSource(props: {  readonly sourceId: string;  readonly onSave: () => void;}) {  const displayScope = useScope();  const scopeStack = useScopeStack();  const userScope = useUserScope();  const sourceSummaryResult = useAtomValue(sourceAtom(props.sourceId, displayScope));  const sourceSummary =    AsyncResult.isSuccess(sourceSummaryResult) && sourceSummaryResult.value      ? sourceSummaryResult.value      : null;  const sourceScopeId = sourceSummary?.scopeId ?? displayScope;  const sourceScope = ScopeId.make(sourceScopeId);  const scopeRanks = useMemo(    () => new Map(scopeStack.map((scope, index) => [scope.id, index] as const)),    [scopeStack],  );   const sourceResult = useAtomValue(openApiSourceAtom(sourceScope, props.sourceId));  const bindingsResult = useAtomValue(    openApiSourceBindingsAtom(displayScope, props.sourceId, sourceScope),  );  const connectionsResult = useAtomValue(connectionsAtom(displayScope));  const secretList = useSecretPickerSecrets();   const doUpdate = useAtomSet(updateOpenApiSource, { mode: "promiseExit" });  const doSetBinding = useAtomSet(setOpenApiSourceBinding, {    mode: "promiseExit",  });  const doRemoveBinding = useAtomSet(removeOpenApiSourceBinding, {    mode: "promiseExit",  });  const doStartOAuth = useAtomSet(startOAuth, { mode: "promiseExit" });  const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({    popupName: OPENAPI_OAUTH_POPUP_NAME,    popupBlockedMessage: "OAuth popup was blocked by the browser",    startErrorMessage: "Failed to connect OAuth",  });   const source =    AsyncResult.isSuccess(sourceResult) && sourceResult.value ? sourceResult.value : null;  const bindingRows: readonly OpenApiSourceBindingRef[] = AsyncResult.isSuccess(bindingsResult)    ? bindingsResult.value    : [];  const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : [];  const oauth2RedirectUrl = oauthCallbackUrl(OPENAPI_OAUTH_CALLBACK_PATH);   const [name, setName] = useState(source?.name ?? "");  const [baseUrl, setBaseUrl] = useState(source?.config.baseUrl ?? "");  const [sourceSaveState, setSourceSaveState] = useState<"idle" | "saving" | "saved">("idle");  const [error, setError] = useState<string | null>(null);  const [busyKey, setBusyKey] = useState<string | null>(null);  const [pendingOAuthConnection, setPendingOAuthConnection] = useState<{    readonly scopeId: ScopeId;    readonly slot: string;    readonly connectionId: string;  } | null>(null);  const [loadedSourceKey, setLoadedSourceKey] = useState<string | null>(null);  const [selectedOAuthTokenScope, setSelectedOAuthTokenScope] = useState<string>(    userScope !== sourceScopeId ? userScope : sourceScopeId,  );  const [oauth2AuthorizationUrl, setOAuth2AuthorizationUrl] = useState(    source?.config.oauth2?.authorizationUrl ?? "",  );  const [oauth2TokenUrl, setOAuth2TokenUrl] = useState(source?.config.oauth2?.tokenUrl ?? "");  const [oauth2EndpointsSaveState, setOAuth2EndpointsSaveState] = useState<    "idle" | "saving" | "saved"  >("idle");  const editIdentity = useMemo(    () => ({      name,      namespace: props.sourceId,      setName,      setNamespace: () => {},      reset: () => {},    }),    [name, props.sourceId],  );  const sourceSaveSeq = useRef(0);  const oauth2EndpointsSaveSeq = useRef(0);   useEffect(() => {    setSelectedOAuthTokenScope(userScope !== sourceScopeId ? userScope : sourceScopeId);  }, [sourceScopeId, userScope]);   useEffect(() => {    if (!source) return;    const sourceKey = `${sourceScopeId}:${source.namespace}`;    if (loadedSourceKey === sourceKey) return;    setName(source.name);    setBaseUrl(source.config.baseUrl ?? "");    setOAuth2AuthorizationUrl(source.config.oauth2?.authorizationUrl ?? "");    setOAuth2TokenUrl(source.config.oauth2?.tokenUrl ?? "");    setOAuth2EndpointsSaveState("idle");    setSourceSaveState("idle");    setLoadedSourceKey(sourceKey);  }, [loadedSourceKey, source, sourceScopeId]);   useEffect(() => {    if (!source) return;    const sourceKey = `${sourceScopeId}:${source.namespace}`;    if (loadedSourceKey !== sourceKey) return;     const nextName = name.trim();    const nextBaseUrl = baseUrl.trim();    const currentName = source.name;    const currentBaseUrl = source.config.baseUrl ?? "";    if ((nextName || currentName) === currentName && nextBaseUrl === currentBaseUrl) {      return;    }     const timeout = window.setTimeout(() => {      const seq = ++sourceSaveSeq.current;      setSourceSaveState("saving");      setError(null);      void (async () => {        const exit = await doUpdate({          params: { scopeId: displayScope, namespace: props.sourceId },          payload: {            sourceScope,            name: nextName || undefined,            baseUrl: nextBaseUrl || undefined,          },          reactivityKeys: openApiWriteKeys,        });        if (sourceSaveSeq.current !== seq) return;        if (Exit.isFailure(exit)) {          setSourceSaveState("idle");          setError(errorMessageFromExit(exit, "Failed to save source details"));          return;        }        setSourceSaveState("saved");        window.setTimeout(() => {          if (sourceSaveSeq.current === seq) setSourceSaveState("idle");        }, 1600);      })();    }, 600);     return () => window.clearTimeout(timeout);  }, [    baseUrl,    displayScope,    doUpdate,    loadedSourceKey,    name,    props.sourceId,    source,    sourceScope,    sourceScopeId,  ]);   const secretSlots = useMemo(() => {    if (!source) return [] as SlotDef[];    const slots: SlotDef[] = [];    for (const [headerName, value] of Object.entries(source.config.headers ?? {})) {      if (typeof value === "string") continue;      slots.push({        kind: "secret",        slot: value.slot,        label: headerName,        hint: value.prefix ? `Prefix: ${value.prefix}` : undefined,      });    }    if (source.config.oauth2) {      const clientSecretSlot = effectiveClientSecretSlot(source.config.oauth2);      slots.push({        kind: "secret",        slot: source.config.oauth2.clientIdSlot,        label: "Client ID",      });      slots.push({        kind: "secret",        slot: clientSecretSlot,        label: "Client Secret",        hint:          source.config.oauth2.flow === "authorizationCode"            ? "Optional for public PKCE clients"            : undefined,      });      slots.push({        kind: "oauth2",        slot: source.config.oauth2.connectionSlot,        label:          source.config.oauth2.flow === "clientCredentials"            ? "OAuth Client Credentials"            : "OAuth Authorization Code",      });    }    return slots;  }, [source]);   const credentialScopes = useMemo(() => {    const entries = [{ scopeId: ScopeId.make(sourceScopeId), label: "Organization" }];    if (userScope !== sourceScopeId) {      entries.unshift({ scopeId: ScopeId.make(userScope), label: "Personal" });    } else {      entries[0] = {        scopeId: ScopeId.make(sourceScopeId),        label: "Credentials",      };    }    return entries;  }, [sourceScopeId, userScope]);  const credentialScopeOptions = useMemo(    () =>      credentialScopes.map((entry) => ({        scopeId: entry.scopeId,        label: entry.label,        description:          entry.label === "Personal"            ? "Saved only for your account."            : "Shared with everyone who can use this source.",      })),    [credentialScopes],  );  const organizationCredentialScope =    credentialScopes.find((entry) => entry.label === "Organization") ?? credentialScopes[0]!;  const personalCredentialScope =    credentialScopes.find((entry) => entry.label === "Personal") ?? null;  const secretBindingScopes =    personalCredentialScope &&    personalCredentialScope.scopeId !== organizationCredentialScope.scopeId      ? [organizationCredentialScope, personalCredentialScope]      : [organizationCredentialScope];  const activeOAuthTokenScope =    credentialScopes.find((entry) => entry.scopeId === selectedOAuthTokenScope) ??    credentialScopes[0]!;  const activeOAuthTokenScopeId = activeOAuthTokenScope.scopeId;  const activeOAuthTokenScopeLabel = activeOAuthTokenScope.label;   if (!source) {    return (      <div className="space-y-3">        <h1 className="text-xl font-semibold text-foreground">Edit OpenAPI Source</h1>        <p className="text-sm text-muted-foreground">Loading configuration…</p>      </div>    );  }   const setSecretBinding = async (    targetScope: ScopeId,    slot: string,    secretId: string,    secretScope: ScopeId,  ) => {    const inputKey = `${targetScope}:${slot}`;    const trimmed = secretId.trim();    if (!trimmed) return;    setBusyKey(inputKey);    setError(null);    const exit = await doSetBinding({      params: { scopeId: displayScope },      payload: new OpenApiSourceBindingInput({        sourceId: props.sourceId,        sourceScope,        scope: targetScope,        slot,        value: {          kind: "secret",          secretId: SecretId.make(trimmed),          secretScopeId: secretScope,        },      }),      reactivityKeys: sourceWriteKeys,    });    if (Exit.isFailure(exit)) {      setError(errorMessageFromExit(exit, "Failed to save credential binding"));    }    setBusyKey(null);  };   const clearBinding = async (targetScope: ScopeId, slot: string) => {    setBusyKey(`${targetScope}:${slot}:clear`);    setError(null);    const exit = await doRemoveBinding({      params: { scopeId: displayScope },      payload: {        sourceId: props.sourceId,        sourceScope,        slot,        scope: targetScope,      },      reactivityKeys: sourceWriteKeys,    });    if (Exit.isFailure(exit)) {      setError(errorMessageFromExit(exit, "Failed to clear credential binding"));    }    setBusyKey(null);  };   const connectOAuth = async (targetScope: ScopeId) => {    const oauth2 = source.config.oauth2;    if (!oauth2) return;    const clientIdBinding = effectiveCredentialBindingForScope(      bindingRows,      oauth2.clientIdSlot,      targetScope,      scopeRanks,    );    const clientSecretSlot = effectiveClientSecretSlot(oauth2);    const clientSecretBinding = effectiveCredentialBindingForScope(      bindingRows,      clientSecretSlot,      targetScope,      scopeRanks,    );    if (!clientIdBinding || !isSecretCredentialBindingValue(clientIdBinding.value)) {      setError("Client ID must be bound before connecting");      return;    }    const clientIdSecretId = clientIdBinding.value.secretId;    if (      oauth2.flow === "clientCredentials" &&      (!clientSecretBinding || !isSecretCredentialBindingValue(clientSecretBinding.value))    ) {      setError("Client secret must be bound before connecting");      return;    }    const clientSecretValue =      oauth2.flow === "clientCredentials" &&      clientSecretBinding &&      isSecretCredentialBindingValue(clientSecretBinding.value)        ? clientSecretBinding.value        : null;     const existingConnection = exactCredentialBindingForScope(      bindingRows,      oauth2.connectionSlot,      targetScope,    );    const connectionId =      existingConnection && isConnectionCredentialBindingValue(existingConnection.value)        ? existingConnection.value.connectionId        : openApiOAuthConnectionId(props.sourceId, oauth2.securitySchemeName, targetScope);     setBusyKey(`${targetScope}:${oauth2.connectionSlot}:connect`);    setPendingOAuthConnection({      scopeId: targetScope,      slot: oauth2.connectionSlot,      connectionId: connectionId,    });    setError(null);    const failConnect = (message: string) => {      setError(message);      setPendingOAuthConnection(null);      setBusyKey(null);    };    const displayName = source.name;    const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? "");    if (oauth2.flow === "clientCredentials") {      const startOAuthExit = await doStartOAuth({        params: { scopeId: targetScope },        payload: {          endpoint: tokenUrl,          redirectUrl: tokenUrl,          connectionId: connectionId,          tokenScope: targetScope,          strategy: {            kind: "client-credentials",            tokenEndpoint: tokenUrl,            clientIdSecretId,            clientSecretSecretId: clientSecretValue!.secretId,            scopes: [...oauth2.scopes],          },          pluginId: "openapi",          identityLabel: `${displayName} OAuth`,        },      });      if (Exit.isFailure(startOAuthExit)) {        failConnect(errorMessageFromExit(startOAuthExit, "Failed to connect OAuth"));        return;      }      const response = startOAuthExit.value;      if (!response.completedConnection) {        failConnect("Unexpected OAuth response");        return;      }      const setBindingExit = await doSetBinding({        params: { scopeId: displayScope },        payload: new OpenApiSourceBindingInput({          sourceId: props.sourceId,          sourceScope,          scope: targetScope,          slot: oauth2.connectionSlot,          value: {            kind: "connection",            connectionId: ConnectionId.make(response.completedConnection.connectionId),

Lines 1–500 of 842.