/**
 * MCP OAuth Provider
 *
 * Implementation of the MCP SDK's OAuthClientProvider interface.
 * Handles OAuth client registration, token storage, and authorization redirection.
 */
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
import { getAuthEntry, getAuthForUrl, updateTokens, updateClientInfo, updateCodeVerifier, updateOAuthState, clearAllCredentials, clearClientInfo, clearTokens, } from "./mcp-auth.js";
// Callback server configuration
const DEFAULT_OAUTH_CALLBACK_PORT = 19876;
const OAUTH_CALLBACK_PATH = "/callback";
let configuredOAuthCallbackPort = DEFAULT_OAUTH_CALLBACK_PORT;
if (process.env.MCP_OAUTH_CALLBACK_PORT) {
    const parsedPort = Number.parseInt(process.env.MCP_OAUTH_CALLBACK_PORT, 10);
    if (Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
        configuredOAuthCallbackPort = parsedPort;
    }
}
let oauthCallbackPort = configuredOAuthCallbackPort;
export function getConfiguredOAuthCallbackPort() {
    return configuredOAuthCallbackPort;
}
export function getOAuthCallbackPort() {
    return oauthCallbackPort;
}
export function setOAuthCallbackPort(port) {
    oauthCallbackPort = port;
}
/**
 * OAuth provider implementation for MCP servers.
 * Implements the OAuthClientProvider interface from the MCP SDK.
 */
export class McpOAuthProvider {
    serverName;
    serverUrl;
    config;
    callbacks;
    constructor(serverName, serverUrl, config, callbacks) {
        this.serverName = serverName;
        this.serverUrl = serverUrl;
        this.config = config;
        this.callbacks = callbacks;
    }
    get usesClientCredentials() {
        return this.config.grantType === "client_credentials";
    }
    /**
     * The redirect URL for OAuth callbacks.
     * Uses configured redirectUri if provided, otherwise falls back to default.
     */
    get redirectUrl() {
        if (this.usesClientCredentials)
            return undefined;
        if (this.config.redirectUri)
            return this.config.redirectUri;
        return `http://localhost:${getOAuthCallbackPort()}${OAUTH_CALLBACK_PATH}`;
    }
    /**
     * Client metadata for dynamic registration.
     * Describes this client to the OAuth authorization server.
     */
    get clientMetadata() {
        if (this.usesClientCredentials) {
            return {
                client_name: "Pi Coding Agent",
                redirect_uris: [],
                grant_types: ["client_credentials"],
                token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
            };
        }
        const redirectUrl = this.redirectUrl;
        if (!redirectUrl) {
            throw new Error("redirectUrl is required for authorization_code flow");
        }
        return {
            redirect_uris: [redirectUrl],
            client_name: "Pi Coding Agent",
            client_uri: "https://github.com/nicobailon/spectral-mcp-adapter",
            grant_types: ["authorization_code", "refresh_token"],
            response_types: ["code"],
            token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
        };
    }
    /**
     * Get client information (for pre-registered or dynamically registered clients).
     * Returns undefined if no client info exists or if the server URL has changed.
     */
    async clientInformation() {
        // Check config first (pre-registered client)
        if (this.config.clientId) {
            return {
                client_id: this.config.clientId,
                client_secret: this.config.clientSecret,
            };
        }
        // Check stored client info (from dynamic registration)
        // Use getAuthForUrl to validate credentials are for the current server URL
        const entry = await getAuthForUrl(this.serverName, this.serverUrl);
        if (entry?.clientInfo) {
            // Check if client secret has expired
            if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
                return undefined;
            }
            return {
                client_id: entry.clientInfo.clientId,
                client_secret: entry.clientInfo.clientSecret,
            };
        }
        // No client info or URL changed - will trigger dynamic registration
        return undefined;
    }
    /**
     * Save client information from dynamic registration.
     */
    async saveClientInformation(info) {
        const clientInfo = {
            clientId: info.client_id,
            clientSecret: info.client_secret,
            clientIdIssuedAt: info.client_id_issued_at,
            clientSecretExpiresAt: info.client_secret_expires_at,
        };
        updateClientInfo(this.serverName, clientInfo, this.serverUrl);
    }
    /**
     * Get stored OAuth tokens.
     * Returns undefined if no tokens exist or if the server URL has changed.
     */
    async tokens() {
        // Use getAuthForUrl to validate tokens are for the current server URL
        const entry = await getAuthForUrl(this.serverName, this.serverUrl);
        if (!entry?.tokens)
            return undefined;
        return {
            access_token: entry.tokens.accessToken,
            token_type: "Bearer",
            refresh_token: entry.tokens.refreshToken,
            expires_in: entry.tokens.expiresAt
                ? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000))
                : undefined,
            scope: entry.tokens.scope,
        };
    }
    /**
     * Save OAuth tokens.
     */
    async saveTokens(tokens) {
        const storedTokens = {
            accessToken: tokens.access_token,
            refreshToken: tokens.refresh_token,
            expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
            scope: tokens.scope,
        };
        updateTokens(this.serverName, storedTokens, this.serverUrl);
    }
    /**
     * Redirect the user to the authorization URL.
     * This opens the browser for the user to authenticate.
     *
     * Throws UnauthorizedError when called outside of a user-initiated flow
     * (no oauthState saved by startAuth). That path is reached when the SDK
     * falls through from a failed refresh into a fresh authorization_code
     * flow, which library hosts cannot complete in-process.
     */
    async redirectToAuthorization(authorizationUrl) {
        if (this.usesClientCredentials) {
            throw new Error("redirectToAuthorization is not used for client_credentials flow");
        }
        // No saved oauthState means we're on the post-refresh authorize fallback.
        const entry = await getAuthEntry(this.serverName);
        if (!entry?.oauthState) {
            throw new UnauthorizedError(`Re-authentication required for MCP server: ${this.serverName}`);
        }
        // URL is passed to callback, not logged (may contain sensitive params)
        await this.callbacks.onRedirect(authorizationUrl);
    }
    /**
     * Save the PKCE code verifier.
     */
    async saveCodeVerifier(codeVerifier) {
        updateCodeVerifier(this.serverName, codeVerifier);
    }
    /**
     * Get the stored PKCE code verifier.
     * @throws Error if no code verifier is stored
     */
    async codeVerifier() {
        if (this.usesClientCredentials) {
            throw new Error("codeVerifier is not used for client_credentials flow");
        }
        const entry = await getAuthEntry(this.serverName);
        if (!entry?.codeVerifier) {
            throw new Error(`No code verifier saved for MCP server: ${this.serverName}`);
        }
        return entry.codeVerifier;
    }
    /**
     * Save the OAuth state parameter for CSRF protection.
     */
    async saveState(state) {
        updateOAuthState(this.serverName, state);
    }
    /**
     * Get the stored OAuth state parameter.
     * @throws UnauthorizedError if no flow is in progress (see redirectToAuthorization)
     */
    async state() {
        if (this.usesClientCredentials) {
            throw new Error("state is not used for client_credentials flow");
        }
        const entry = await getAuthEntry(this.serverName);
        if (!entry?.oauthState) {
            throw new UnauthorizedError(`Re-authentication required for MCP server: ${this.serverName}`);
        }
        return entry.oauthState;
    }
    /**
     * Invalidate credentials when authentication fails.
     * Clears tokens, client info, or all credentials based on the type.
     */
    async invalidateCredentials(type) {
        switch (type) {
            case "all":
                clearAllCredentials(this.serverName);
                break;
            case "client":
                clearClientInfo(this.serverName);
                break;
            case "tokens":
                clearTokens(this.serverName);
                break;
        }
    }
    prepareTokenRequest(scope) {
        if (!this.usesClientCredentials) {
            return undefined;
        }
        const params = new URLSearchParams({ grant_type: "client_credentials" });
        const requestedScope = scope ?? this.config.scope;
        if (requestedScope) {
            params.set("scope", requestedScope);
        }
        return params;
    }
}
export { DEFAULT_OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH };
