762 lines · 27.4 KB · TypeScript
RawDownload
import { useEffect, useMemo, useRef, useState } from "react";import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react"; import { openOAuthPopup, type OAuthPopupResult } from "@executor/plugin-oauth2/react";import {  connectionsAtom,  sourceAtom,} from "@executor/react/api/atoms";import { useScope, useScopeStack, useUserScope } from "@executor/react/api/scope-context";import {  connectionWriteKeys,  sourceWriteKeys,} from "@executor/react/api/reactivity-keys";import { Button } from "@executor/react/components/button";import { CopyButton } from "@executor/react/components/copy-button";import {  CardStack,  CardStackContent,  CardStackEntry,  CardStackEntryContent,  CardStackEntryDescription,  CardStackEntryField,  CardStackEntryTitle,} from "@executor/react/components/card-stack";import { FilterTabs } from "@executor/react/components/filter-tabs";import { Input } from "@executor/react/components/input";import { sourceWriteKeys as openApiWriteKeys } from "@executor/react/api/reactivity-keys";import { ConnectionId, ScopeId, SecretId } from "@executor/sdk";import { CreatableSecretPicker } from "@executor/react/plugins/secret-header-auth";import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets"; import {  openApiSourceAtom,  openApiSourceBindingsAtom,  removeOpenApiSourceBinding,  setOpenApiSourceBinding,  startOpenApiOAuth,  updateOpenApiSource,} from "./atoms";import {  OPENAPI_OAUTH_CALLBACK_PATH,  OPENAPI_OAUTH_CHANNEL,  OPENAPI_OAUTH_POPUP_NAME,  resolveOAuthUrl,} from "./AddOpenApiSource";import { oauth2ClientSecretSlot } from "../sdk/store";import type { OpenApiSourceBindingValue } from "../sdk/types"; 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 bindingSecretId = (sourceId: string, slot: string, scopeId: string): string =>  `source-binding-${slugify(sourceId)}-${slugify(slot)}-${slugify(scopeId)}`; const effectiveClientSecretSlot = (oauth2: {  readonly securitySchemeName: string;  readonly clientSecretSlot: string | null;}): string => oauth2.clientSecretSlot ?? oauth2ClientSecretSlot(oauth2.securitySchemeName); const exactBindingForScope = (  rows: readonly {    readonly slot: string;    readonly scopeId: ScopeId;    readonly value: unknown;  }[],  slot: string,  scopeId: ScopeId,) => rows.find((row) => row.slot === slot && row.scopeId === scopeId) ?? null; const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: ScopeId): number =>  ranks.get(scopeId as string) ?? Number.MAX_SAFE_INTEGER; const effectiveBindingForScope = (  rows: readonly {    readonly slot: string;    readonly scopeId: ScopeId;    readonly value: unknown;  }[],  slot: string,  targetScope: ScopeId,  ranks: ReadonlyMap<string, number>,) =>  rows.find(    (row) =>      row.slot === slot &&      scopeRank(ranks, row.scopeId) >= scopeRank(ranks, targetScope),  ) ?? null; const isSecretBindingValue = (  value: unknown,): value is Extract<OpenApiSourceBindingValue, { readonly kind: "secret" }> =>  typeof value === "object" &&  value !== null &&  "kind" in value &&  (value as { kind?: unknown }).kind === "secret" &&  "secretId" in value; const isConnectionBindingValue = (  value: unknown,): value is Extract<OpenApiSourceBindingValue, { readonly kind: "connection" }> =>  typeof value === "object" &&  value !== null &&  "kind" in value &&  (value as { kind?: unknown }).kind === "connection" &&  "connectionId" in value; 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 =    Result.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 as string, 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: "promise" });  const doSetBinding = useAtomSet(setOpenApiSourceBinding, { mode: "promise" });  const doRemoveBinding = useAtomSet(removeOpenApiSourceBinding, { mode: "promise" });  const doStartOAuth = useAtomSet(startOpenApiOAuth, { mode: "promise" });   const source =    Result.isSuccess(sourceResult) && sourceResult.value ? sourceResult.value : null;  const bindingRows =    Result.isSuccess(bindingsResult) ? bindingsResult.value : [];  const connections =    Result.isSuccess(connectionsResult) ? connectionsResult.value : [];  const oauth2RedirectUrl =    typeof window !== "undefined"      ? `${window.location.origin}${OPENAPI_OAUTH_CALLBACK_PATH}`      : 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 [loadedSourceKey, setLoadedSourceKey] = useState<string | null>(null);  const [selectedCredentialScope, setSelectedCredentialScope] = useState<string>(    userScope !== sourceScopeId ? userScope : sourceScopeId,  );  const sourceSaveSeq = useRef(0);   useEffect(() => {    if (!source) return;    const sourceKey = `${sourceScopeId}:${source.namespace}`;    if (loadedSourceKey === sourceKey) return;    setName(source.name);    setBaseUrl(source.config.baseUrl ?? "");    setSourceSaveState("idle");    setLoadedSourceKey(sourceKey);  }, [loadedSourceKey, source, sourceScopeId]);   useEffect(() => {    setSelectedCredentialScope(userScope !== sourceScopeId ? userScope : sourceScopeId);  }, [sourceScopeId, userScope]);   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 doUpdate({        path: { scopeId: ScopeId.make(sourceScopeId), namespace: props.sourceId },        payload: {          name: nextName || undefined,          baseUrl: nextBaseUrl || undefined,          headers: source.config.headers,          oauth2: source.config.oauth2,        },        reactivityKeys: openApiWriteKeys,      })        .then(() => {          if (sourceSaveSeq.current !== seq) return;          setSourceSaveState("saved");          window.setTimeout(() => {            if (sourceSaveSeq.current === seq) setSourceSaveState("idle");          }, 1600);        })        .catch((e: unknown) => {          if (sourceSaveSeq.current !== seq) return;          setSourceSaveState("idle");          setError(e instanceof Error ? e.message : "Failed to save source details");        });    }, 600);     return () => window.clearTimeout(timeout);  }, [    baseUrl,    doUpdate,    loadedSourceKey,    name,    props.sourceId,    source,    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 activeCredentialScope =    credentialScopes.find((entry) => entry.scopeId === selectedCredentialScope) ??    credentialScopes[0]!;  const activeCredentialScopeId = activeCredentialScope.scopeId;  const activeCredentialScopeLabel = activeCredentialScope.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,  ) => {    const inputKey = `${targetScope}:${slot}`;    const trimmed = secretId.trim();    if (!trimmed) return;    setBusyKey(inputKey);    setError(null);    try {      await doSetBinding({        path: { scopeId: displayScope },        payload: {          sourceId: props.sourceId,          sourceScope,          scope: targetScope,          slot,          value: { kind: "secret", secretId: SecretId.make(trimmed) },        },        reactivityKeys: sourceWriteKeys,      });    } catch (e) {      setError(e instanceof Error ? e.message : "Failed to save credential binding");    } finally {      setBusyKey(null);    }  };   const clearBinding = async (targetScope: ScopeId, slot: string) => {    setBusyKey(`${targetScope}:${slot}:clear`);    setError(null);    try {      await doRemoveBinding({        path: { scopeId: displayScope },        payload: {          sourceId: props.sourceId,          sourceScope,          slot,          scope: targetScope,        },        reactivityKeys: sourceWriteKeys,      });    } catch (e) {      setError(e instanceof Error ? e.message : "Failed to clear credential binding");    } finally {      setBusyKey(null);    }  };   const connectOAuth = async (targetScope: ScopeId) => {    const oauth2 = source.config.oauth2;    if (!oauth2) return;    const clientIdBinding = effectiveBindingForScope(      bindingRows,      oauth2.clientIdSlot,      targetScope,      scopeRanks,    );    const clientSecretSlot = effectiveClientSecretSlot(oauth2);    const clientSecretBinding = effectiveBindingForScope(      bindingRows,      clientSecretSlot,      targetScope,      scopeRanks,    );    if (!clientIdBinding || !isSecretBindingValue(clientIdBinding.value)) {      setError("Client ID must be bound before connecting");      return;    }    if (oauth2.flow === "clientCredentials" && (!clientSecretBinding || !isSecretBindingValue(clientSecretBinding.value))) {      setError("Client secret must be bound before connecting");      return;    }    const clientSecretValue =      oauth2.flow === "clientCredentials" &&      clientSecretBinding &&      isSecretBindingValue(clientSecretBinding.value)        ? clientSecretBinding.value        : null;     const existingConnection = exactBindingForScope(      bindingRows,      oauth2.connectionSlot,      targetScope,    );    const connectionId =      existingConnection && isConnectionBindingValue(existingConnection.value)        ? existingConnection.value.connectionId        : ConnectionId.make(            `openapi-oauth-${slugify(props.sourceId)}-${slugify(oauth2.securitySchemeName)}-${slugify(targetScope)}`,          );     setBusyKey(`${targetScope}:${oauth2.connectionSlot}:connect`);    setError(null);    try {      const displayName = source.name;      const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? "");      if (oauth2.flow === "clientCredentials") {        const response = await doStartOAuth({          path: { scopeId: displayScope },          payload: {            sourceId: props.sourceId,            connectionId,            tokenScope: targetScope,            displayName,            securitySchemeName: oauth2.securitySchemeName,            flow: "clientCredentials",            tokenUrl,            clientIdSecretId: clientIdBinding.value.secretId,            clientSecretSecretId: clientSecretValue!.secretId,            scopes: [...oauth2.scopes],          },        });        if (response.flow !== "clientCredentials") {          throw new Error("Unexpected OAuth response");        }        await doSetBinding({          path: { scopeId: displayScope },          payload: {            sourceId: props.sourceId,            sourceScope,            scope: targetScope,            slot: oauth2.connectionSlot,            value: {              kind: "connection",              connectionId: ConnectionId.make(response.auth.connectionId),            },          },          reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],        });        return;      }       const authorizationUrl = resolveOAuthUrl(        oauth2.authorizationUrl ?? "",        source.config.baseUrl ?? "",      );      const redirectUrl =        typeof window !== "undefined"          ? `${window.location.origin}${OPENAPI_OAUTH_CALLBACK_PATH}`          : OPENAPI_OAUTH_CALLBACK_PATH;      const response = await doStartOAuth({        path: { scopeId: displayScope },        payload: {          sourceId: props.sourceId,          connectionId,          tokenScope: targetScope,          displayName,          securitySchemeName: oauth2.securitySchemeName,          flow: "authorizationCode",          authorizationUrl,          tokenUrl,          redirectUrl,          clientIdSecretId: clientIdBinding.value.secretId,          clientSecretSecretId:            clientSecretBinding && isSecretBindingValue(clientSecretBinding.value)              ? clientSecretBinding.value.secretId              : undefined,          scopes: [...oauth2.scopes],        },      });      if (response.flow !== "authorizationCode") {        throw new Error("Unexpected OAuth response");      }      openOAuthPopup({        url: response.authorizationUrl,        popupName: OPENAPI_OAUTH_POPUP_NAME,        channelName: OPENAPI_OAUTH_CHANNEL,        onResult: async (result: OAuthPopupResult<{ connectionId: string }>) => {          if (!result.ok) {            setError(result.error);            setBusyKey(null);            return;          }          try {            await doSetBinding({              path: { scopeId: displayScope },              payload: {                sourceId: props.sourceId,                sourceScope,                scope: targetScope,                slot: oauth2.connectionSlot,                value: {                  kind: "connection",                  connectionId: ConnectionId.make(result.connectionId),                },              },              reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],            });          } catch (e) {            setError(e instanceof Error ? e.message : "Failed to save OAuth binding");          } finally {            setBusyKey(null);

Lines 1–500 of 762.