* 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>
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import type { CallbackParams } from "./types.js";
|
|
|
|
/**
|
|
* Parse a string as an absolute URL. On failure, throws with `label` and the
|
|
* offending value so callers (and UI toasts) can show what to fix.
|
|
*/
|
|
export function parseHttpUrl(value: string, label: string): URL {
|
|
const trimmed = value.trim();
|
|
try {
|
|
return new URL(trimmed);
|
|
} catch (err) {
|
|
const detail = err instanceof Error ? err.message : String(err);
|
|
throw new Error(`Invalid ${label}: "${trimmed}" (${detail})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses OAuth 2.1 callback parameters from a URL search string
|
|
* @param location The URL search string (e.g., "?code=abc123" or "?error=access_denied")
|
|
* @returns Parsed callback parameters with success/error information
|
|
*/
|
|
export const parseOAuthCallbackParams = (location: string): CallbackParams => {
|
|
const params = new URLSearchParams(location);
|
|
|
|
const code = params.get("code");
|
|
if (code) {
|
|
return { successful: true, code };
|
|
}
|
|
|
|
const error = params.get("error");
|
|
const error_description = params.get("error_description");
|
|
const error_uri = params.get("error_uri");
|
|
|
|
if (error) {
|
|
return { successful: false, error, error_description, error_uri };
|
|
}
|
|
|
|
return {
|
|
successful: false,
|
|
error: "invalid_request",
|
|
error_description: "Missing code or error in response",
|
|
error_uri: null,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Generate a random state for the OAuth 2.0 flow.
|
|
* Works in both browser and Node.js environments.
|
|
*
|
|
* @returns A random state for the OAuth 2.0 flow.
|
|
*/
|
|
export const generateOAuthState = (): string => {
|
|
// Generate a random state
|
|
const array = new Uint8Array(32);
|
|
|
|
// Use crypto.getRandomValues (available in both browser and Node.js)
|
|
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
crypto.getRandomValues(array);
|
|
} else {
|
|
// Fallback for environments without crypto.getRandomValues
|
|
// This should not happen in modern environments
|
|
for (let i = 0; i < array.length; i++) {
|
|
array[i] = Math.floor(Math.random() * 256);
|
|
}
|
|
}
|
|
|
|
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
"",
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Parse OAuth `state` to extract the auth session id (CSRF token).
|
|
* Must be the 64-char hex value from {@link generateOAuthState}.
|
|
*/
|
|
export const parseOAuthState = (state: string): { authId: string } | null => {
|
|
if (!state || typeof state !== "string") return null;
|
|
if (/^[a-f0-9]{64}$/i.test(state)) {
|
|
return { authId: state };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Generates a human-readable error description from OAuth callback error parameters
|
|
* @param params OAuth error callback parameters containing error details
|
|
* @returns Formatted multiline error message with error code, description, and optional URI
|
|
*/
|
|
export const generateOAuthErrorDescription = (
|
|
params: Extract<CallbackParams, { successful: false }>,
|
|
): string => {
|
|
const error = params.error;
|
|
const errorDescription = params.error_description;
|
|
const errorUri = params.error_uri;
|
|
|
|
return [
|
|
`Error: ${error}.`,
|
|
errorDescription ? `Details: ${errorDescription}.` : "",
|
|
errorUri ? `More info: ${errorUri}.` : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
};
|
|
|
|
/**
|
|
* True when a thrown connect error represents an upstream 401. The remote
|
|
* transport preserves the status on the error object; as a fallback, match
|
|
* transport wording `"failed …(401)"` so unrelated `(401)` in messages does
|
|
* not trigger OAuth.
|
|
*/
|
|
export function isUnauthorizedError(err: unknown): boolean {
|
|
if (typeof err === "object" && err !== null) {
|
|
const status = (err as { status?: number; code?: number }).status;
|
|
const code = (err as { code?: number }).code;
|
|
if (status === 401 || code === 401) return true;
|
|
}
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return /\bfailed\b[^\n]*\(401\)/i.test(message);
|
|
}
|