export const isApiKey = (creds) => {
    return typeof creds === 'string' || creds instanceof ApiKey;
};
export const mapApiKey = (creds) => {
    return creds instanceof ApiKey ? creds : new ApiKey(creds);
};
export class OidcAuthenticator {
    http;
    creds;
    accessToken;
    refreshToken;
    expiresAt;
    refreshRunning;
    refreshInterval;
    constructor(http, creds) {
        this.http = http;
        this.creds = creds;
        this.accessToken = '';
        this.refreshToken = '';
        this.expiresAt = 0;
        this.refreshRunning = false;
        // If the authentication method is access token,
        // our bearer token is already available for use
        if (this.creds instanceof AuthAccessTokenCredentials) {
            this.accessToken = this.creds.accessToken;
            this.expiresAt = this.creds.expiresAt;
            this.refreshToken = this.creds.refreshToken;
        }
    }
    refresh = async (localConfig) => {
        const config = await this.getOpenidConfig(localConfig);
        let authenticator;
        switch (this.creds.constructor) {
            case AuthUserPasswordCredentials:
                authenticator = new UserPasswordAuthenticator(this.http, this.creds, config);
                break;
            case AuthAccessTokenCredentials:
                authenticator = new AccessTokenAuthenticator(this.http, this.creds, config);
                break;
            case AuthClientCredentials:
                authenticator = new ClientCredentialsAuthenticator(this.http, this.creds, config);
                break;
            default:
                throw new Error('unsupported credential type');
        }
        return authenticator.refresh().then((resp) => {
            this.accessToken = resp.accessToken;
            this.expiresAt = resp.expiresAt;
            this.refreshToken = resp.refreshToken;
            this.startTokenRefresh(authenticator);
        });
    };
    getOpenidConfig = (localConfig) => {
        return this.http.externalGet(localConfig.href).then((openidProviderConfig) => {
            const scopes = localConfig.scopes || [];
            return {
                clientId: localConfig.clientId,
                provider: openidProviderConfig,
                scopes: scopes,
            };
        });
    };
    startTokenRefresh = (authenticator) => {
        if (this.creds.silentRefresh && !this.refreshRunning && this.refreshTokenProvided()) {
            this.refreshInterval = setInterval(async () => {
                // check every 30s if the token will expire in <= 1m,
                // if so, refresh
                if (this.expiresAt - Date.now() <= 60_000) {
                    const resp = await authenticator.refresh();
                    this.accessToken = resp.accessToken;
                    this.expiresAt = resp.expiresAt;
                    this.refreshToken = resp.refreshToken;
                }
            }, 30_000);
            this.refreshRunning = true;
        }
    };
    stopTokenRefresh = () => {
        clearInterval(this.refreshInterval);
        this.refreshRunning = false;
    };
    refreshTokenProvided = () => {
        return this.refreshToken && this.refreshToken != '';
    };
    getAccessToken = () => {
        return this.accessToken;
    };
    getExpiresAt = () => {
        return this.expiresAt;
    };
    resetExpiresAt() {
        this.expiresAt = 0;
    }
}
export class AuthUserPasswordCredentials {
    username;
    password;
    scopes;
    silentRefresh;
    constructor(creds) {
        this.username = creds.username;
        this.password = creds.password;
        this.scopes = creds.scopes;
        this.silentRefresh = parseSilentRefresh(creds.silentRefresh);
    }
}
class UserPasswordAuthenticator {
    creds;
    http;
    openidConfig;
    constructor(http, creds, config) {
        this.http = http;
        this.creds = creds;
        this.openidConfig = config;
        if (creds.scopes) {
            this.openidConfig.scopes.push(creds.scopes);
        }
    }
    refresh = () => {
        this.validateOpenidConfig();
        return this.requestAccessToken()
            .then((tokenResp) => {
            return {
                accessToken: tokenResp.access_token,
                expiresAt: calcExpirationEpoch(tokenResp.expires_in),
                refreshToken: tokenResp.refresh_token,
            };
        })
            .catch((err) => {
            return Promise.reject(new Error(`failed to refresh access token: ${err}`));
        });
    };
    validateOpenidConfig = () => {
        if (this.openidConfig.provider.grant_types_supported !== undefined &&
            !this.openidConfig.provider.grant_types_supported.includes('password')) {
            throw new Error('grant_type password not supported');
        }
        if (this.openidConfig.provider.token_endpoint.includes('https://login.microsoftonline.com')) {
            throw new Error('microsoft/azure recommends to avoid authentication using ' +
                'username and password, so this method is not supported by this client');
        }
        this.openidConfig.scopes.push('offline_access');
    };
    requestAccessToken = () => {
        const url = this.openidConfig.provider.token_endpoint;
        const params = new URLSearchParams({
            grant_type: 'password',
            client_id: this.openidConfig.clientId,
            username: this.creds.username,
            password: this.creds.password,
            scope: this.openidConfig.scopes.join(' '),
        });
        const contentType = 'application/x-www-form-urlencoded;charset=UTF-8';
        return this.http.externalPost(url, params, contentType);
    };
}
export class AuthAccessTokenCredentials {
    accessToken;
    expiresAt;
    refreshToken;
    silentRefresh;
    constructor(creds) {
        this.validate(creds);
        this.accessToken = creds.accessToken;
        this.expiresAt = calcExpirationEpoch(creds.expiresIn);
        this.refreshToken = creds.refreshToken;
        this.silentRefresh = parseSilentRefresh(creds.silentRefresh);
    }
    validate = (creds) => {
        if (creds.expiresIn === undefined) {
            throw new Error('AuthAccessTokenCredentials: expiresIn is required');
        }
        if (!Number.isInteger(creds.expiresIn) || creds.expiresIn <= 0) {
            throw new Error('AuthAccessTokenCredentials: expiresIn must be int > 0');
        }
    };
}
class AccessTokenAuthenticator {
    creds;
    http;
    openidConfig;
    constructor(http, creds, config) {
        this.http = http;
        this.creds = creds;
        this.openidConfig = config;
    }
    refresh = () => {
        if (this.creds.refreshToken === undefined || this.creds.refreshToken == '') {
            console.warn('AuthAccessTokenCredentials not provided with refreshToken, cannot refresh');
            return Promise.resolve({
                accessToken: this.creds.accessToken,
                expiresAt: this.creds.expiresAt,
            });
        }
        this.validateOpenidConfig();
        return this.requestAccessToken()
            .then((tokenResp) => {
            return {
                accessToken: tokenResp.access_token,
                expiresAt: calcExpirationEpoch(tokenResp.expires_in),
                refreshToken: tokenResp.refresh_token,
            };
        })
            .catch((err) => {
            return Promise.reject(new Error(`failed to refresh access token: ${err}`));
        });
    };
    validateOpenidConfig = () => { };
    requestAccessToken = () => {
        const url = this.openidConfig.provider.token_endpoint;
        const params = new URLSearchParams({
            grant_type: 'refresh_token',
            client_id: this.openidConfig.clientId,
            refresh_token: this.creds.refreshToken,
        });
        const contentType = 'application/x-www-form-urlencoded;charset=UTF-8';
        return this.http.externalPost(url, params, contentType);
    };
}
export class AuthClientCredentials {
    clientSecret;
    scopes;
    silentRefresh;
    constructor(creds) {
        this.clientSecret = creds.clientSecret;
        this.scopes = creds.scopes;
        this.silentRefresh = parseSilentRefresh(creds.silentRefresh);
    }
}
class ClientCredentialsAuthenticator {
    creds;
    http;
    openidConfig;
    constructor(http, creds, config) {
        this.http = http;
        this.creds = creds;
        this.openidConfig = config;
        if (creds.scopes) {
            this.openidConfig.scopes.push(creds.scopes);
        }
    }
    refresh = () => {
        this.validateOpenidConfig();
        return this.requestAccessToken()
            .then((tokenResp) => {
            return {
                accessToken: tokenResp.access_token,
                expiresAt: calcExpirationEpoch(tokenResp.expires_in),
                refreshToken: tokenResp.refresh_token,
            };
        })
            .catch((err) => {
            return Promise.reject(new Error(`failed to refresh access token: ${err}`));
        });
    };
    validateOpenidConfig = () => {
        if (this.openidConfig.scopes.length > 0) {
            return;
        }
        if (this.openidConfig.provider.token_endpoint.includes('https://login.microsoftonline.com')) {
            this.openidConfig.scopes.push(this.openidConfig.clientId + '/.default');
        }
    };
    requestAccessToken = () => {
        const url = this.openidConfig.provider.token_endpoint;
        const params = new URLSearchParams({
            grant_type: 'client_credentials',
            client_id: this.openidConfig.clientId,
            client_secret: this.creds.clientSecret,
            scope: this.openidConfig.scopes.join(' '),
        });
        const contentType = 'application/x-www-form-urlencoded;charset=UTF-8';
        return this.http.externalPost(url, params, contentType);
    };
}
export class ApiKey {
    apiKey;
    constructor(apiKey) {
        this.apiKey = apiKey;
    }
}
function calcExpirationEpoch(expiresIn) {
    return Date.now() + (expiresIn - 2) * 1000; // -2 for some lag
}
function parseSilentRefresh(silentRefresh) {
    // Silent token refresh by default
    if (silentRefresh === undefined) {
        return true;
    }
    else {
        return silentRefresh;
    }
}
