import { discoverOAuthServerInfo, extractWWWAuthenticateParams } from '@modelcontextprotocol/sdk/client/auth.js';
import { getHostedMcpOAuthProviderId } from './hosted-oauth-id.js';
import { withHostedMcpProxyFetchOptions } from './proxy-fetch.js';
const DEFAULT_DISCOVERY_TIMEOUT_MS = 30_000;
function normalizeScope(scope) {
    if (!scope) {
        return undefined;
    }
    const unique = [
        ...new Set(scope
            .split(/\s+/)
            .map((value) => value.trim())
            .filter(Boolean))
    ];
    return unique.length > 0 ? unique.join(' ') : undefined;
}
function normalizeSupportedScopes(scopes) {
    if (!Array.isArray(scopes)) {
        return undefined;
    }
    const supported = scopes.filter((value) => typeof value === 'string' && value.trim().length > 0);
    return supported.length > 0 ? [...new Set(supported)].join(' ') : undefined;
}
async function probeUnauthorizedChallenge(serverUrl, timeoutMs) {
    try {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), timeoutMs);
        if (typeof timeout === 'object' && 'unref' in timeout)
            timeout.unref();
        const init = withHostedMcpProxyFetchOptions(serverUrl, {
            method: 'GET',
            redirect: 'manual',
            signal: controller.signal,
            headers: {
                accept: 'application/json, text/plain;q=0.9, */*;q=0.1',
                'mcp-protocol-version': '2024-11-05'
            }
        });
        const response = await fetch(serverUrl, init);
        clearTimeout(timeout);
        return extractWWWAuthenticateParams(response);
    }
    catch {
        return {};
    }
}
function resolveDiscoveryTimeoutMs(options) {
    return options?.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
}
function createTimedFetch(timeoutMs) {
    return async (input, init) => {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), timeoutMs);
        if (typeof timeout === 'object' && 'unref' in timeout)
            timeout.unref();
        const signal = init?.signal
            ? AbortSignal.any([init.signal, controller.signal])
            : controller.signal;
        try {
            return await fetch(input, withHostedMcpProxyFetchOptions(input, { ...init, signal }));
        }
        finally {
            clearTimeout(timeout);
        }
    };
}
export function resolveHostedMcpOAuthTimeoutMs(config) {
    return config.startupTimeoutMs ?? config.timeout ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
}
function urlsMatch(left, right) {
    if (!left) {
        return false;
    }
    try {
        return new URL(left).toString() === new URL(right).toString();
    }
    catch {
        return false;
    }
}
function discoveryMatchesServer(discovery, serverUrl) {
    if (!discovery) {
        return false;
    }
    if (urlsMatch(discovery.resourceMetadata?.resource, serverUrl)) {
        return true;
    }
    try {
        const serverOrigin = new URL(serverUrl).origin;
        const metadataUrl = discovery.resourceMetadataUrl ?? discovery.resourceMetadata?.resource ?? undefined;
        return metadataUrl ? new URL(metadataUrl).origin === serverOrigin : false;
    }
    catch {
        return false;
    }
}
export async function resolveHostedMcpOAuth(serverName, config, mcpOAuthStore, options) {
    const oauth = config.oauth ?? {};
    const timeoutMs = resolveDiscoveryTimeoutMs(options);
    const providerId = getHostedMcpOAuthProviderId(serverName, config);
    const rawCachedDiscovery = mcpOAuthStore?.getDiscoveryState(providerId);
    const cachedDiscovery = discoveryMatchesServer(rawCachedDiscovery, config.url)
        ? rawCachedDiscovery
        : undefined;
    if (rawCachedDiscovery && !cachedDiscovery) {
        mcpOAuthStore?.removeDiscoveryState(providerId);
        mcpOAuthStore?.removeClientInfo(providerId);
    }
    const configuredResourceMetadataUrl = oauth.resourceMetadataUrl
        ? new URL(oauth.resourceMetadataUrl)
        : undefined;
    const challenge = !configuredResourceMetadataUrl || !oauth.scope
        ? await probeUnauthorizedChallenge(config.url, timeoutMs)
        : undefined;
    // Resolution precedence:
    // 1. Explicit config
    // 2. Cached or freshly discovered metadata
    // 3. Unauthorized challenge fallback
    const effectiveResourceMetadataUrl = configuredResourceMetadataUrl ??
        (cachedDiscovery?.resourceMetadataUrl
            ? new URL(cachedDiscovery.resourceMetadataUrl)
            : undefined) ??
        challenge?.resourceMetadataUrl;
    let discoveryState = cachedDiscovery;
    if (!discoveryState) {
        try {
            const discovered = await discoverOAuthServerInfo(config.url, {
                ...(effectiveResourceMetadataUrl
                    ? { resourceMetadataUrl: effectiveResourceMetadataUrl }
                    : {}),
                fetchFn: createTimedFetch(timeoutMs)
            });
            discoveryState = {
                authorizationServerUrl: discovered.authorizationServerUrl,
                ...(discovered.authorizationServerMetadata
                    ? { authorizationServerMetadata: discovered.authorizationServerMetadata }
                    : {}),
                ...(discovered.resourceMetadata ? { resourceMetadata: discovered.resourceMetadata } : {}),
                ...(effectiveResourceMetadataUrl
                    ? { resourceMetadataUrl: effectiveResourceMetadataUrl.toString() }
                    : {})
            };
            mcpOAuthStore?.setDiscoveryState(providerId, discoveryState);
        }
        catch {
            discoveryState = cachedDiscovery;
        }
    }
    const resourceMetadataUrl = configuredResourceMetadataUrl ??
        (discoveryState?.resourceMetadataUrl
            ? new URL(discoveryState.resourceMetadataUrl)
            : undefined) ??
        challenge?.resourceMetadataUrl;
    return {
        discoveryState,
        resourceMetadataUrl,
        resource: oauth.resource ??
            (typeof discoveryState?.resourceMetadata?.resource === 'string'
                ? discoveryState.resourceMetadata.resource
                : undefined),
        scope: normalizeScope(oauth.scope) ??
            normalizeSupportedScopes(discoveryState?.resourceMetadata?.scopes_supported) ??
            normalizeScope(challenge?.scope)
    };
}
export async function supportsHostedMcpOAuth(serverName, config, mcpOAuthStore, options) {
    if (config.oauth) {
        return true;
    }
    const resolved = await resolveHostedMcpOAuth(serverName, config, mcpOAuthStore, options);
    return !!(resolved.discoveryState?.authorizationServerUrl ||
        resolved.resourceMetadataUrl ||
        resolved.scope);
}
//# sourceMappingURL=oauth-discovery.js.map