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. Load more
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);