import React, { useState, useEffect, useRef, useCallback } from "react"; import { Box, Text, useInput, type Key } from "ink"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import { SelectableItem } from "./SelectableItem.js"; import type { MCPServerConfig, InspectorClient, ConnectionStatus, } from "@inspector/core/mcp/index.js"; import type { OAuthConnectionState } from "@inspector/core/auth/types.js"; import type { AuthChallenge } from "@inspector/core/auth/challenge.js"; import { formatAuthProtocol, formatClientRegistrationKind, formatIdpSession, formatScopes, } from "../utils/oauthDisplay.js"; import { stepUpAuthorizeActionLabel, stepUpConfirmMessage, stepUpFollowUpMessage, stepUpModalTitle, } from "../utils/tuiOAuth.js"; interface AuthTabProps { serverName: string | null; serverConfig: MCPServerConfig | null; inspectorClient: InspectorClient | null; oauthStatus: "idle" | "authenticating" | "error"; oauthMessage: string | null; oauthRevision: number; pendingStepUp?: { challenge: AuthChallenge; authorizationScopes?: string[]; enterpriseManaged?: boolean; } | null; onAuthorizeStepUp?: () => void; onCancelStepUp?: () => void; width: number; height: number; focused?: boolean; onClearOAuth: () => void; connectionStatus: ConnectionStatus; } function OAuthDetailRow({ label, value }: { label: string; value: string }) { return ( {label}: {value} ); } export function AuthTab({ serverName, inspectorClient, oauthStatus, oauthMessage, oauthRevision, pendingStepUp, onAuthorizeStepUp, onCancelStepUp, width, height, focused = false, onClearOAuth, connectionStatus, }: AuthTabProps) { const isLiveConnection = connectionStatus === "connected" || connectionStatus === "connecting"; const scrollViewRef = useRef(null); const [oauthState, setOauthState] = useState< OAuthConnectionState | undefined >(undefined); const [clearedConfirmation, setClearedConfirmation] = useState(false); const [lastClearDisconnected, setLastClearDisconnected] = useState(false); const [stepUpChoiceIndex, setStepUpChoiceIndex] = useState(0); const refreshOAuthState = useCallback(async () => { if (!inspectorClient) { setOauthState(undefined); return; } const state = await inspectorClient.getOAuthState(); setOauthState(state); }, [inspectorClient]); useEffect(() => { void refreshOAuthState(); }, [refreshOAuthState, oauthRevision, connectionStatus]); useEffect(() => { setClearedConfirmation(false); setLastClearDisconnected(false); }, [oauthRevision]); useEffect(() => { setStepUpChoiceIndex(0); }, [pendingStepUp]); useEffect(() => { if (!inspectorClient) return; const update = () => { void refreshOAuthState(); }; inspectorClient.addEventListener("oauthComplete", update); return () => { inspectorClient.removeEventListener("oauthComplete", update); }; }, [inspectorClient, refreshOAuthState]); useInput( (input: string, key: Key) => { if (!focused) return; if (pendingStepUp) { if (key.upArrow) { setStepUpChoiceIndex((i) => Math.max(0, i - 1)); return; } if (key.downArrow) { setStepUpChoiceIndex((i) => Math.min(1, i + 1)); return; } if (key.return) { if (stepUpChoiceIndex === 0) { onAuthorizeStepUp?.(); } else { onCancelStepUp?.(); } return; } if (input.toLowerCase() === "a") { onAuthorizeStepUp?.(); return; } if (input.toLowerCase() === "c") { onCancelStepUp?.(); return; } return; } if (key.upArrow && scrollViewRef.current) { scrollViewRef.current.scrollBy(-1); } else if (key.downArrow && scrollViewRef.current) { scrollViewRef.current.scrollBy(1); } else if (key.pageUp && scrollViewRef.current) { const h = scrollViewRef.current.getViewportHeight() || 1; scrollViewRef.current.scrollBy(-h); } else if (key.pageDown && scrollViewRef.current) { const h = scrollViewRef.current.getViewportHeight() || 1; scrollViewRef.current.scrollBy(h); } else if (input.toLowerCase() === "s") { setLastClearDisconnected(isLiveConnection); onClearOAuth(); setClearedConfirmation(true); } }, { isActive: focused }, ); if (!serverName) { return ( Select a server to view authentication. ); } const scopes = oauthState ? formatScopes(oauthState) : undefined; const accessToken = oauthState?.tokens?.access_token; return ( OAuth {oauthStatus === "authenticating" && ( Authenticating… )} {oauthStatus === "error" && oauthMessage && ( {oauthMessage} )} {oauthStatus === "idle" && oauthMessage && ( {oauthMessage} )} {pendingStepUp ? ( {stepUpModalTitle({ enterpriseManaged: pendingStepUp.enterpriseManaged, })} {stepUpConfirmMessage(pendingStepUp.challenge, { enterpriseManaged: pendingStepUp.enterpriseManaged, })}{" "} {stepUpFollowUpMessage({ enterpriseManaged: pendingStepUp.enterpriseManaged, })} {(() => { const scopes = pendingStepUp.authorizationScopes ?? pendingStepUp.challenge.requiredScopes; if (!scopes?.length) return null; return ( {pendingStepUp.enterpriseManaged ? "Permissions requested:" : "Scopes requested:"} {scopes.map((scope) => ( {" "} • {scope} ))} ); })()} {(() => { const label = stepUpAuthorizeActionLabel({ enterpriseManaged: pendingStepUp.enterpriseManaged, }); return ( <> {label[0]} {label.slice(1)} ); })()} Cancel ) : null} {oauthState ? ( OAuth Details {oauthState.client?.clientId && ( )} {oauthState.client?.registrationKind && ( )} {oauthState.protocol === "ema" && oauthState.ema?.idpSession && ( )} {oauthState.authorizationServerMetadata ?.authorization_endpoint && ( )} {scopes && } {accessToken && ( )} ) : ( oauthStatus !== "authenticating" && ( No OAuth information yet. Connect (C) to authorize when this server requires it. ) )} Clear OAuth State {isLiveConnection && " and disconnect"} {clearedConfirmation && ( {lastClearDisconnected ? "OAuth state cleared. Disconnected." : "OAuth state cleared."} )} {focused && ( {pendingStepUp ? "↑/↓ select, Enter confirm, A authorize, C cancel" : `S ${isLiveConnection ? "clear+disconnect" : "clear"}, ↑/↓ scroll`} )} ); }