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`}
)}
);
}