"use strict";
/*
 * Copyright 2025 Matti Airas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.setFetchFunction = setFetchFunction;
exports.resetFetchFunction = resetFetchFunction;
exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
exports.fetchUserinfo = fetchUserinfo;
const types_1 = require("./types");
let fetchFn = fetch;
/**
 * Set the fetch function (for testing)
 */
function setFetchFunction(fn) {
    fetchFn = fn;
}
/**
 * Reset the fetch function to the default
 */
function resetFetchFunction() {
    fetchFn = fetch;
}
/**
 * Exchange an authorization code for tokens
 * @param code The authorization code from the callback
 * @param config OIDC configuration
 * @param metadata Discovery document metadata
 * @param authState The auth state containing code verifier
 * @returns The token response
 * @throws OIDCError if the exchange fails
 */
async function exchangeAuthorizationCode(code, config, metadata, authState) {
    const params = new URLSearchParams();
    params.set('grant_type', 'authorization_code');
    params.set('code', code);
    params.set('redirect_uri', authState.redirectUri);
    params.set('client_id', config.clientId);
    params.set('client_secret', config.clientSecret);
    params.set('code_verifier', authState.codeVerifier);
    let response;
    try {
        response = await fetchFn(metadata.token_endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: params.toString()
        });
    }
    catch (err) {
        throw new types_1.OIDCError(`Token exchange request failed: ${err}`, 'TOKEN_EXCHANGE_FAILED', err instanceof Error ? err : undefined);
    }
    let body;
    try {
        body = (await response.json());
    }
    catch (err) {
        throw new types_1.OIDCError('Failed to parse token response as JSON', 'TOKEN_EXCHANGE_FAILED', err instanceof Error ? err : undefined);
    }
    if (!response.ok) {
        const errorMessage = body.error_description || body.error || 'Unknown error';
        throw new types_1.OIDCError(`Token exchange failed: ${body.error} - ${errorMessage}`, 'TOKEN_EXCHANGE_FAILED');
    }
    // Validate response structure
    if (!body.access_token || !body.id_token) {
        throw new types_1.OIDCError('Token response missing required fields (access_token, id_token)', 'TOKEN_EXCHANGE_FAILED');
    }
    return {
        accessToken: body.access_token,
        idToken: body.id_token,
        refreshToken: body.refresh_token,
        expiresIn: body.expires_in,
        tokenType: body.token_type || 'Bearer'
    };
}
/**
 * Fetch additional claims from the userinfo endpoint
 *
 * The ID token typically only contains minimal claims (sub, iss, aud, etc.).
 * Claims like groups, email, name are returned via the userinfo endpoint.
 *
 * @param accessToken The access token from the token response
 * @param metadata Discovery document metadata containing userinfo_endpoint
 * @param issuer The configured OIDC issuer URL (for hostname validation)
 * @returns Additional claims from the userinfo endpoint, or undefined if unavailable
 */
async function fetchUserinfo(accessToken, metadata, issuer) {
    // Not all providers have a userinfo endpoint
    if (!metadata.userinfo_endpoint) {
        return undefined;
    }
    // Security: validate userinfo endpoint hostname matches issuer hostname
    // This prevents a malicious discovery document from redirecting userinfo
    // requests to an attacker-controlled server
    try {
        const issuerHost = new URL(issuer).hostname;
        const userinfoHost = new URL(metadata.userinfo_endpoint).hostname;
        if (issuerHost !== userinfoHost) {
            console.warn(`OIDC: Userinfo endpoint hostname (${userinfoHost}) does not match issuer hostname (${issuerHost})`);
            return undefined;
        }
    }
    catch (err) {
        console.warn(`OIDC: Failed to validate userinfo endpoint URL: ${err}`);
        return undefined;
    }
    let response;
    try {
        response = await fetchFn(metadata.userinfo_endpoint, {
            method: 'GET',
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        });
    }
    catch (err) {
        // Non-fatal: continue with ID token claims only
        console.warn(`OIDC: Failed to fetch userinfo: ${err instanceof Error ? err.message : err}`);
        return undefined;
    }
    if (!response.ok) {
        console.warn(`OIDC: Userinfo request failed with status ${response.status}`);
        return undefined;
    }
    try {
        return (await response.json());
    }
    catch (err) {
        console.warn(`OIDC: Failed to parse userinfo response: ${err instanceof Error ? err.message : err}`);
        return undefined;
    }
}
//# sourceMappingURL=token-exchange.js.map