Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)
* refactor(web): update OAuth E2E tests to use new authentication method - Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration. - Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs. - Ensured that the tests continue to validate the expected authorization URL after the change. Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process. * Guided-auth removal, OAuth smoke testing, and TUI/CLI support * Added CLI OAuth runner flags tests * Added clarifying comment on port 0 binding * README notes on TUI oauth callback port, prettier fixes for CLI tests * CIMD validation uses inline like EMA IdP * Fixed TUI test failures from merge * test: bring 5 OAuth files over the 90 per-file coverage gate The guided-auth-removal work added/touched several files that fell below the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run the gate (it's local-only), so validate + test:integration passed while these gaps slipped through. Bring them all back over the line: - core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the provably-dead guards (empty host, out-of-range port, String(err) fallback, pathname "/" fallback) -- all rejected by new URL() upstream -- with justified v8 ignore comments instead of lowering the gate. - core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return. - core/react/useClientSettingsDraft.ts: test the functional-updater path, the stale-onChange-after-close prev===null guard, and the timer-fires-after-close value===null guard. - ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear. - clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration kinds, the expired IdP session, and the configured-scope/empty-scope branches of formatScopes. npm run coverage now passes (exit 0) across web, cli, tui, launcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cliffhall <cliff@futurescale.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4a927d3978
commit
13dafe457b
1 changed files with 5702 additions and 4444 deletions
|
|
@ -179,6 +179,8 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
private outputValidator: AjvJsonSchemaValidator | null = null;
|
||||
private transport: Transport | MessageTrackingTransport | null = null;
|
||||
private baseTransport: Transport | null = null;
|
||||
/** True when the cached transport was built with an OAuth authProvider attached. */
|
||||
private transportHasAuthProvider = false;
|
||||
private pipeStderr: boolean;
|
||||
private initialLoggingLevel?: LoggingLevel;
|
||||
private sample: boolean;
|
||||
|
|
@ -308,8 +310,6 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
initialConfig: oauthConfig,
|
||||
enterpriseManagedAuth: options.enterpriseManagedAuth,
|
||||
installEnterpriseManagedAuth: options.installEnterpriseManagedAuth,
|
||||
dispatchOAuthStepChange: (detail) =>
|
||||
this.dispatchTypedEvent("oauthStepChange", detail),
|
||||
dispatchOAuthComplete: (detail) =>
|
||||
this.dispatchTypedEvent("oauthComplete", detail),
|
||||
dispatchOAuthAuthorizationRequired: (detail) =>
|
||||
|
|
@ -680,6 +680,26 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
return updatedTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the cached MCP transport without a full disconnect() teardown.
|
||||
* Used when a pre-auth connect failed or tokens arrived after an unauthenticated
|
||||
* transport was created, so the next connect() can attach authProvider.
|
||||
*/
|
||||
private async dropCachedTransport(): Promise<void> {
|
||||
if (!this.baseTransport && !this.transport) {
|
||||
this.transportHasAuthProvider = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.client?.close();
|
||||
} catch {
|
||||
// Ignore errors on close
|
||||
}
|
||||
this.baseTransport = null;
|
||||
this.transport = null;
|
||||
this.transportHasAuthProvider = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the MCP server
|
||||
*/
|
||||
|
|
@ -691,6 +711,18 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
return;
|
||||
}
|
||||
|
||||
const oauthManager = this.oauthManager;
|
||||
if (
|
||||
this.baseTransport &&
|
||||
this.isHttpOAuthConfig() &&
|
||||
oauthManager &&
|
||||
!this.transportHasAuthProvider &&
|
||||
!oauthManager.isEnterpriseManaged() &&
|
||||
(await oauthManager.isOAuthAuthorized())
|
||||
) {
|
||||
await this.dropCachedTransport();
|
||||
}
|
||||
|
||||
// Create transport (single place for create / wrap / attach).
|
||||
if (!this.baseTransport) {
|
||||
const transportOptions: CreateTransportOptions = {
|
||||
|
|
@ -707,13 +739,10 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
},
|
||||
...(this.serverSettings && { settings: this.serverSettings }),
|
||||
};
|
||||
const oauthManager = this.oauthManager;
|
||||
if (this.isHttpOAuthConfig() && oauthManager) {
|
||||
if (oauthManager.isEnterpriseManaged()) {
|
||||
await oauthManager.trySilentEnterpriseManagedAuth();
|
||||
}
|
||||
const provider = await oauthManager.createOAuthProviderForTransport();
|
||||
if (oauthManager.isEnterpriseManaged()) {
|
||||
const provider = await oauthManager.createOAuthProviderForTransport();
|
||||
const tokens = await provider.tokens();
|
||||
if (!tokens?.access_token) {
|
||||
const err = new Error(
|
||||
|
|
@ -723,9 +752,16 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
err.code = 401;
|
||||
throw err;
|
||||
}
|
||||
transportOptions.authProvider = provider;
|
||||
} else if (await oauthManager.isOAuthAuthorized()) {
|
||||
// Without stored tokens, omit authProvider so connect() surfaces a plain
|
||||
// 401 instead of the SDK opening a browser before the app callback
|
||||
// server is listening (TUI/CLI run authenticate() explicitly).
|
||||
transportOptions.authProvider =
|
||||
await oauthManager.createOAuthProviderForTransport();
|
||||
}
|
||||
transportOptions.authProvider = provider;
|
||||
}
|
||||
this.transportHasAuthProvider = !!transportOptions.authProvider;
|
||||
const { transport: baseTransport } = this.transportClientFactory(
|
||||
this.transportConfig,
|
||||
transportOptions,
|
||||
|
|
@ -1082,6 +1118,9 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
} catch (error) {
|
||||
this.status = "error";
|
||||
this.dispatchTypedEvent("statusChange", this.status);
|
||||
if (this.baseTransport && !this.transportHasAuthProvider) {
|
||||
await this.dropCachedTransport();
|
||||
}
|
||||
// Deliberately do NOT dispatch the `error` event here: this is the
|
||||
// awaited `connect()` path, so re-throwing hands the reason straight to
|
||||
// the caller. The `error` event is reserved for non-awaited transitions
|
||||
|
|
@ -1143,6 +1182,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
// Null out transport so next connect() creates a fresh one.
|
||||
this.baseTransport = null;
|
||||
this.transport = null;
|
||||
this.transportHasAuthProvider = false;
|
||||
// Update status - any onclose fired during close() above deferred to us
|
||||
// (see `disconnecting`), so this is the single place the explicit-disconnect
|
||||
// path settles the status and emits `disconnect`.
|
||||
|
|
@ -2673,54 +2713,15 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
}
|
||||
|
||||
/**
|
||||
* Initiates OAuth flow using SDK's auth() function (normal mode)
|
||||
* Can be called directly by user or automatically triggered by 401 errors
|
||||
* Initiates OAuth flow. Can be called directly by user or automatically
|
||||
* triggered by 401 errors.
|
||||
*/
|
||||
async authenticate(): Promise<URL | undefined> {
|
||||
return this.ensureOAuthManager().authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts guided OAuth flow (step-by-step). Runs only the first step.
|
||||
* Use proceedOAuthStep() to advance. When oauthStep is "authorization_code",
|
||||
* set authorizationCode and call proceedOAuthStep() to complete.
|
||||
*/
|
||||
async beginGuidedAuth(): Promise<void> {
|
||||
return this.ensureOAuthManager().beginGuidedAuth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs guided OAuth flow to completion. If already started (via beginGuidedAuth),
|
||||
* continues from current step. Otherwise initializes and runs from the start.
|
||||
* Returns the authorization URL when user must authorize, or undefined if already complete.
|
||||
*/
|
||||
async runGuidedAuth(): Promise<URL | undefined> {
|
||||
return this.ensureOAuthManager().runGuidedAuth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authorization code for guided OAuth flow.
|
||||
* Validates that the client is in guided OAuth mode (has active state machine).
|
||||
* @param authorizationCode The authorization code from the OAuth callback
|
||||
* @param completeFlow If true, automatically proceed through all remaining steps to completion.
|
||||
* If false, only set the code and wait for manual progression via proceedOAuthStep().
|
||||
* Defaults to false for manual step-by-step control.
|
||||
* @throws Error if not in guided OAuth flow or not at authorization_code step
|
||||
*/
|
||||
async setGuidedAuthorizationCode(
|
||||
authorizationCode: string,
|
||||
completeFlow: boolean = false,
|
||||
): Promise<void> {
|
||||
return this.ensureOAuthManager().setGuidedAuthorizationCode(
|
||||
authorizationCode,
|
||||
completeFlow,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes OAuth flow with authorization code.
|
||||
* For guided mode, this calls setGuidedAuthorizationCode(code, true) internally.
|
||||
* For normal mode, uses SDK auth() directly.
|
||||
* Completes OAuth flow with authorization code from the redirect callback.
|
||||
*/
|
||||
async completeOAuthFlow(authorizationCode: string): Promise<void> {
|
||||
return this.ensureOAuthManager().completeOAuthFlow(authorizationCode);
|
||||
|
|
@ -2754,17 +2755,14 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
}
|
||||
|
||||
/**
|
||||
* In-memory OAuth flow snapshot (quick or guided). Undefined when no flow
|
||||
* has run on this client instance; use {@link getOAuthState} for persisted
|
||||
* authorization state.
|
||||
* In-memory OAuth flow snapshot. Undefined when no flow has run on this
|
||||
* client instance; use {@link getOAuthState} for persisted authorization state.
|
||||
*/
|
||||
getOAuthFlowState(): OAuthFlowState | undefined {
|
||||
return this.oauthManager?.getOAuthFlowState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current step when an OAuth flow is active (quick or guided).
|
||||
*/
|
||||
/** Current step when an OAuth flow is active. */
|
||||
getOAuthFlowStep(): OAuthStep | undefined {
|
||||
return this.oauthManager?.getOAuthFlowStep();
|
||||
}
|
||||
|
|
@ -2779,11 +2777,4 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
}
|
||||
return this.oauthManager.getOAuthState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually progress to next step in guided OAuth flow
|
||||
*/
|
||||
async proceedOAuthStep(): Promise<void> {
|
||||
return this.ensureOAuthManager().proceedOAuthStep();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue