
import { decodeBase64 } from './buffer.js';

const WELL_KNOWN_ROUTE = '/.well-known';
const AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH = `${WELL_KNOWN_ROUTE}/oauth-protected-resource`;
const AUTH_SERVER_METADATA_DISCOVERY_PATH = `${WELL_KNOWN_ROUTE}/oauth-authorization-server`;
const OPENID_CONNECT_DISCOVERY_PATH = `${WELL_KNOWN_ROUTE}/openid-configuration`;
const AUTH_SCOPE_SEPARATOR = ' ';
var AuthorizationErrorType;
(function (AuthorizationErrorType) {
    AuthorizationErrorType["InvalidRequest"] = "invalid_request";
    AuthorizationErrorType["InvalidClient"] = "invalid_client";
    AuthorizationErrorType["InvalidGrant"] = "invalid_grant";
    AuthorizationErrorType["UnauthorizedClient"] = "unauthorized_client";
    AuthorizationErrorType["UnsupportedGrantType"] = "unsupported_grant_type";
    AuthorizationErrorType["InvalidScope"] = "invalid_scope";
})(AuthorizationErrorType || (AuthorizationErrorType = {}));
var AuthorizationDeviceCodeErrorType;
(function (AuthorizationDeviceCodeErrorType) {
    AuthorizationDeviceCodeErrorType["AuthorizationPending"] = "authorization_pending";
    AuthorizationDeviceCodeErrorType["SlowDown"] = "slow_down";
    AuthorizationDeviceCodeErrorType["AccessDenied"] = "access_denied";
    AuthorizationDeviceCodeErrorType["ExpiredToken"] = "expired_token";
})(AuthorizationDeviceCodeErrorType || (AuthorizationDeviceCodeErrorType = {}));
var AuthorizationRegistrationErrorType;
(function (AuthorizationRegistrationErrorType) {
    AuthorizationRegistrationErrorType["InvalidRedirectUri"] = "invalid_redirect_uri";
    AuthorizationRegistrationErrorType["InvalidClientMetadata"] = "invalid_client_metadata";
    AuthorizationRegistrationErrorType["InvalidSoftwareStatement"] = "invalid_software_statement";
    AuthorizationRegistrationErrorType["UnapprovedSoftwareStatement"] = "unapproved_software_statement";
})(AuthorizationRegistrationErrorType || (AuthorizationRegistrationErrorType = {}));
function isAuthorizationProtectedResourceMetadata(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const metadata = obj;
    if (!metadata.resource) {
        return false;
    }
    if (metadata.scopes_supported !== undefined && !Array.isArray(metadata.scopes_supported)) {
        return false;
    }
    return true;
}
const urisToCheck = [
    'issuer',
    'authorization_endpoint',
    'token_endpoint',
    'registration_endpoint',
    'jwks_uri'
];
function isAuthorizationServerMetadata(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const metadata = obj;
    if (!metadata.issuer) {
        throw ( new Error('Authorization server metadata must have an issuer'));
    }
    for (const uri of urisToCheck) {
        if (!metadata[uri]) {
            continue;
        }
        if (typeof metadata[uri] !== 'string') {
            throw ( new Error(`Authorization server metadata '${uri}' must be a string`));
        }
        if (!metadata[uri].startsWith('https://') && !metadata[uri].startsWith('http://')) {
            throw ( new Error(
                `Authorization server metadata '${uri}' must start with http:// or https://`
            ));
        }
    }
    return true;
}
function isAuthorizationDynamicClientRegistrationResponse(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const response = obj;
    return response.client_id !== undefined;
}
function isAuthorizationTokenResponse(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const response = obj;
    return response.access_token !== undefined && response.token_type !== undefined;
}
function isAuthorizationErrorResponse(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const response = obj;
    return response.error !== undefined;
}
function isAuthorizationRegistrationErrorResponse(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return false;
    }
    const response = obj;
    return response.error !== undefined;
}
function getDefaultMetadataForUrl(authorizationServer) {
    return {
        issuer: ( authorizationServer.toString()),
        authorization_endpoint: ( ( new URL('/authorize', authorizationServer)).toString()),
        token_endpoint: ( ( new URL('/token', authorizationServer)).toString()),
        registration_endpoint: ( ( new URL('/register', authorizationServer)).toString()),
        response_types_supported: ['code', 'id_token', 'id_token token'],
    };
}
const grantTypesSupported = ['authorization_code', 'refresh_token', 'urn:ietf:params:oauth:grant-type:device_code'];
const DEFAULT_AUTH_FLOW_PORT = 33418;
async function fetchDynamicRegistration(serverMetadata, clientName, scopes) {
    if (!serverMetadata.registration_endpoint) {
        throw ( new Error('Server does not support dynamic registration'));
    }
    const response = await fetch(serverMetadata.registration_endpoint, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            client_name: clientName,
            client_uri: 'https://code.visualstudio.com',
            grant_types: serverMetadata.grant_types_supported
                ? serverMetadata.grant_types_supported.filter(gt => grantTypesSupported.includes(gt))
                : grantTypesSupported,
            response_types: ['code'],
            redirect_uris: [
                'https://insiders.vscode.dev/redirect',
                'https://vscode.dev/redirect',
                'http://127.0.0.1/',
                `http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}/`
            ],
            scope: scopes?.join(AUTH_SCOPE_SEPARATOR),
            token_endpoint_auth_method: 'none',
            application_type: 'native'
        })
    });
    if (!response.ok) {
        const result = await response.text();
        let errorDetails = result;
        try {
            const errorResponse = JSON.parse(result);
            if (isAuthorizationRegistrationErrorResponse(errorResponse)) {
                errorDetails = `${errorResponse.error}${errorResponse.error_description ? `: ${errorResponse.error_description}` : ''}`;
            }
        }
        catch {
        }
        throw ( new Error(
            `Registration to ${serverMetadata.registration_endpoint} failed: ${errorDetails}`
        ));
    }
    const registration = await response.json();
    if (isAuthorizationDynamicClientRegistrationResponse(registration)) {
        return registration;
    }
    throw ( new Error(
        `Invalid authorization dynamic client registration response: ${JSON.stringify(registration)}`
    ));
}
function parseWWWAuthenticateHeader(wwwAuthenticateHeaderValue) {
    const challenges = [];
    const tokens = [];
    let current = '';
    let inQuotes = false;
    for (let i = 0; i < wwwAuthenticateHeaderValue.length; i++) {
        const char = wwwAuthenticateHeaderValue[i];
        if (char === '"') {
            inQuotes = !inQuotes;
            current += char;
        }
        else if (char === ',' && !inQuotes) {
            if (current.trim()) {
                tokens.push(current.trim());
            }
            current = '';
        }
        else {
            current += char;
        }
    }
    if (current.trim()) {
        tokens.push(current.trim());
    }
    let currentChallenge;
    for (const token of tokens) {
        const hasEquals = token.includes('=');
        if (!hasEquals) {
            if (currentChallenge) {
                challenges.push(currentChallenge);
            }
            currentChallenge = { scheme: token.trim(), params: {} };
        }
        else {
            const spaceIndex = token.indexOf(' ');
            if (spaceIndex > 0) {
                const beforeSpace = token.substring(0, spaceIndex);
                const afterSpace = token.substring(spaceIndex + 1);
                if (!beforeSpace.includes('=') && afterSpace.includes('=')) {
                    if (currentChallenge) {
                        challenges.push(currentChallenge);
                    }
                    currentChallenge = { scheme: beforeSpace.trim(), params: {} };
                    const equalIndex = afterSpace.indexOf('=');
                    if (equalIndex > 0) {
                        const key = afterSpace.substring(0, equalIndex).trim();
                        const value = afterSpace.substring(equalIndex + 1).trim().replace(/^"|"$/g, '');
                        if (key && value !== undefined) {
                            currentChallenge.params[key] = value;
                        }
                    }
                    continue;
                }
            }
            if (currentChallenge) {
                const equalIndex = token.indexOf('=');
                if (equalIndex > 0) {
                    const key = token.substring(0, equalIndex).trim();
                    const value = token.substring(equalIndex + 1).trim().replace(/^"|"$/g, '');
                    if (key && value !== undefined) {
                        currentChallenge.params[key] = value;
                    }
                }
            }
        }
    }
    if (currentChallenge) {
        challenges.push(currentChallenge);
    }
    return challenges;
}
function getClaimsFromJWT(token) {
    const parts = token.split('.');
    if (parts.length !== 3) {
        throw ( new Error('Invalid JWT token format: token must have three parts separated by dots'));
    }
    const [header, payload] = parts;
    try {
        const decodedHeader = JSON.parse(( decodeBase64(header).toString()));
        if (typeof decodedHeader !== 'object') {
            throw ( new Error('Invalid JWT token format: header is not a JSON object'));
        }
        const decodedPayload = JSON.parse(( decodeBase64(payload).toString()));
        if (typeof decodedPayload !== 'object') {
            throw ( new Error('Invalid JWT token format: payload is not a JSON object'));
        }
        return decodedPayload;
    }
    catch (e) {
        if (e instanceof Error) {
            throw ( new Error(`Failed to parse JWT token: ${e.message}`));
        }
        throw ( new Error('Failed to parse JWT token'));
    }
}
function scopesMatch(scopes1, scopes2) {
    if (scopes1.length !== scopes2.length) {
        return false;
    }
    const sortedScopes1 = [...scopes1].sort();
    const sortedScopes2 = [...scopes2].sort();
    return sortedScopes1.every((scope, index) => scope === sortedScopes2[index]);
}
async function fetchResourceMetadata(targetResource, resourceMetadataUrl, options = {}) {
    const { sameOriginHeaders = {}, fetch: fetchImpl = fetch } = options;
    const targetResourceUrlObj = ( new URL(targetResource));
    let urlsToTry;
    if (!resourceMetadataUrl) {
        const pathComponent = targetResourceUrlObj.pathname === '/' ? undefined : targetResourceUrlObj.pathname;
        const rootUrl = `${targetResourceUrlObj.origin}${AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH}`;
        if (pathComponent) {
            urlsToTry = [
                `${rootUrl}${pathComponent}`,
                rootUrl
            ];
        }
        else {
            urlsToTry = [rootUrl];
        }
    }
    else {
        urlsToTry = [resourceMetadataUrl];
    }
    const errors = [];
    for (const urlToTry of urlsToTry) {
        try {
            let headers = {
                'Accept': 'application/json'
            };
            const resourceMetadataUrlObj = ( new URL(urlToTry));
            if (resourceMetadataUrlObj.origin === targetResourceUrlObj.origin) {
                headers = {
                    ...headers,
                    ...sameOriginHeaders
                };
            }
            const response = await fetchImpl(urlToTry, { method: 'GET', headers });
            if (response.status !== 200) {
                let errorText;
                try {
                    errorText = await response.text();
                }
                catch {
                    errorText = response.statusText;
                }
                errors.push(( new Error(
                    `Failed to fetch resource metadata from ${urlToTry}: ${response.status} ${errorText}`
                )));
                continue;
            }
            const body = await response.json();
            if (isAuthorizationProtectedResourceMetadata(body)) {
                const prmValue = ( ( new URL(body.resource)).toString());
                const targetValue = ( targetResourceUrlObj.toString());
                if (prmValue !== targetValue) {
                    throw ( new Error(
                        `Protected Resource Metadata resource property value "${prmValue}" (length: ${prmValue.length}) does not match target server url "${targetValue}" (length: ${targetValue.length}). These MUST match to follow OAuth spec https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`
                    ));
                }
                return body;
            }
            else {
                errors.push(( new Error(
                    `Invalid resource metadata from ${urlToTry}. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`
                )));
                continue;
            }
        }
        catch (e) {
            errors.push(e instanceof Error ? e : ( new Error(String(e))));
            continue;
        }
    }
    if (errors.length === 1) {
        throw errors[0];
    }
    else {
        throw ( new AggregateError(errors, 'Failed to fetch resource metadata from all attempted URLs'));
    }
}

export { AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH, AUTH_SCOPE_SEPARATOR, AUTH_SERVER_METADATA_DISCOVERY_PATH, AuthorizationDeviceCodeErrorType, AuthorizationErrorType, AuthorizationRegistrationErrorType, DEFAULT_AUTH_FLOW_PORT, OPENID_CONNECT_DISCOVERY_PATH, fetchDynamicRegistration, fetchResourceMetadata, getClaimsFromJWT, getDefaultMetadataForUrl, isAuthorizationDynamicClientRegistrationResponse, isAuthorizationErrorResponse, isAuthorizationProtectedResourceMetadata, isAuthorizationRegistrationErrorResponse, isAuthorizationServerMetadata, isAuthorizationTokenResponse, parseWWWAuthenticateHeader, scopesMatch };
