"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstallProvider = void 0;
const node_url_1 = require("node:url");
const web_api_1 = require("@slack/web-api");
const callback_options_1 = require("./callback-options");
const default_render_html_for_install_path_1 = __importDefault(require("./default-render-html-for-install-path"));
const errors_1 = require("./errors");
const installation_stores_1 = require("./installation-stores");
const logger_1 = require("./logger");
const state_stores_1 = require("./state-stores");
/**
 * InstallProvider Class. Refer to InsallProviderOptions interface for the details of constructor arguments.
 */
class InstallProvider {
    constructor({ clientId, clientSecret, stateSecret = undefined, stateStore = undefined, stateVerification = true, 
    // this option is only for the backward-compatibility with v2.4 and older
    legacyStateVerification = false, stateCookieName = 'slack-app-oauth-state', stateCookieExpirationSeconds = 600, // 10 minutes
    directInstall = false, installationStore = new installation_stores_1.MemoryInstallationStore(), 
    // If installURLOptions is undefined here, handleInstallPath() does not work for you
    installUrlOptions = undefined, renderHtmlForInstallPath = default_render_html_for_install_path_1.default, authVersion = 'v2', logger = undefined, logLevel = undefined, clientOptions = {}, authorizationUrl = 'https://slack.com/oauth/v2/authorize', }) {
        if (clientId === undefined || clientSecret === undefined) {
            throw new errors_1.InstallerInitializationError('You must provide a valid clientId and clientSecret');
        }
        // Setup the logger
        if (typeof logger !== 'undefined') {
            this.logger = logger;
            if (typeof logLevel !== 'undefined') {
                this.logger.debug('The logLevel given to OAuth was ignored as you also gave logger');
            }
        }
        else {
            this.logger = (0, logger_1.getLogger)('OAuth:InstallProvider', logLevel !== null && logLevel !== void 0 ? logLevel : logger_1.LogLevel.INFO, logger);
        }
        this.stateVerification = stateVerification;
        this.legacyStateVerification = legacyStateVerification;
        this.stateCookieName = stateCookieName;
        this.stateCookieExpirationSeconds = stateCookieExpirationSeconds;
        this.directInstall = directInstall;
        if (!stateVerification) {
            this.logger.warn("You've set InstallProvider#stateVerification to false. This flag is intended to enable org-wide app installations from admin pages. If this isn't your scenario, we recommend setting stateVerification to true and starting your OAuth flow from the provided `/slack/install` or your own starting endpoint.");
        }
        // Setup stateStore
        if (stateStore !== undefined) {
            this.stateStore = stateStore;
        }
        else if (this.stateVerification) {
            // if state verification is disabled, state store is not necessary
            if (stateSecret !== undefined) {
                this.stateStore = new state_stores_1.ClearStateStore(stateSecret, this.stateCookieExpirationSeconds);
            }
            else {
                throw new errors_1.InstallerInitializationError('To use the built-in state store you must provide a State Secret');
            }
        }
        this.installationStore = installationStore;
        this.installUrlOptions = installUrlOptions;
        this.renderHtmlForInstallPath = renderHtmlForInstallPath;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.handleCallback = this.handleCallback.bind(this);
        this.authorize = this.authorize.bind(this);
        this.authVersion = authVersion;
        this.authorizationUrl = authorizationUrl;
        if (authorizationUrl !== 'https://slack.com/oauth/v2/authorize' && authVersion === 'v1') {
            this.logger.info('You provided both an authorizationUrl and an authVersion! The authVersion will be ignored in favor of the authorizationUrl.');
        }
        else if (authVersion === 'v1') {
            this.authorizationUrl = 'https://slack.com/oauth/authorize';
        }
        this.clientOptions = Object.assign({ logger, logLevel: this.logger.getLevel() }, clientOptions);
        this.noTokenClient = new web_api_1.WebClient(undefined, this.clientOptions);
    }
    // ------------------------------------------------------
    // Handling incoming requests from Slack API servers
    // ------------------------------------------------------
    /**
     * Fetches data from the installationStore
     */
    async authorize(source) {
        var _a, _b, _c, _d;
        const sourceForLogging = JSON.stringify(source);
        try {
            this.logger.debug(`Starting authorize() execution (source: ${sourceForLogging})`);
            // Note that `queryResult` may unexpectedly include null values for some properties.
            // For example, MongoDB can often save properties as null for some reasons.
            // Inside this method, we should alwayss check if a value is either undefined or null.
            const queryResult = await this.installationStore.fetchInstallation(source, this.logger);
            if (queryResult === undefined || queryResult === null) {
                throw new Error(`Failed fetching data from the Installation Store (source: ${sourceForLogging})`);
            }
            const authResult = {};
            if (queryResult.user) {
                authResult.userToken = queryResult.user.token;
            }
            if ((_a = queryResult.team) === null || _a === void 0 ? void 0 : _a.id) {
                authResult.teamId = queryResult.team.id;
            }
            else if (source === null || source === void 0 ? void 0 : source.teamId) {
                /**
                 * Since queryResult is a org installation, it won't have team.id.
                 * If one was passed in via source, we should add it to the authResult.
                 */
                authResult.teamId = source.teamId;
            }
            if (((_b = queryResult === null || queryResult === void 0 ? void 0 : queryResult.enterprise) === null || _b === void 0 ? void 0 : _b.id) || (source === null || source === void 0 ? void 0 : source.enterpriseId)) {
                authResult.enterpriseId = ((_c = queryResult === null || queryResult === void 0 ? void 0 : queryResult.enterprise) === null || _c === void 0 ? void 0 : _c.id) || (source === null || source === void 0 ? void 0 : source.enterpriseId);
            }
            if (queryResult.bot) {
                authResult.botToken = queryResult.bot.token;
                authResult.botId = queryResult.bot.id;
                authResult.botUserId = queryResult.bot.userId;
                // Token Rotation Enabled (Bot Token)
                if (queryResult.bot.refreshToken) {
                    authResult.botRefreshToken = queryResult.bot.refreshToken;
                    authResult.botTokenExpiresAt = queryResult.bot.expiresAt; // utc, seconds
                }
            }
            // Token Rotation Enabled (User Token)
            if ((_d = queryResult.user) === null || _d === void 0 ? void 0 : _d.refreshToken) {
                authResult.userRefreshToken = queryResult.user.refreshToken;
                authResult.userTokenExpiresAt = queryResult.user.expiresAt; // utc, seconds
            }
            /*
             * Token Rotation (Expiry Check + Refresh)
             * The presence of `(bot|user)TokenExpiresAt` indicates having opted into token rotation.
             * If the token has expired, or will expire within 2 hours, the token is refreshed and
             * the `authResult` and `Installation` are updated with the new values.
             */
            if (authResult.botRefreshToken || authResult.userRefreshToken) {
                const currentUTCSec = Math.floor(Date.now() / 1000); // seconds
                const tokensToRefresh = detectExpiredOrExpiringTokens(authResult, currentUTCSec);
                if (tokensToRefresh.length > 0) {
                    if (queryResult.authVersion !== 'v2') {
                        const errorMessage = 'Unexpected data structure detected. ' +
                            'The data returned by your InstallationStore#fetchInstallation() method must have "authVersion": "v2" ' +
                            'if it has a refresh token';
                        throw new errors_1.UnknownError(errorMessage);
                    }
                    const refreshResponses = await this.refreshExpiringTokens(tokensToRefresh);
                    if (refreshResponses.length) {
                        const installationUpdates = Object.assign({}, queryResult);
                        for (const refreshResp of refreshResponses) {
                            const tokenType = refreshResp.token_type;
                            // Update Authorization
                            if (tokenType === 'bot') {
                                authResult.botToken = refreshResp.access_token;
                                authResult.botRefreshToken = refreshResp.refresh_token;
                                authResult.botTokenExpiresAt = currentUTCSec + refreshResp.expires_in;
                            }
                            if (tokenType === 'user') {
                                authResult.userToken = refreshResp.access_token;
                                authResult.userRefreshToken = refreshResp.refresh_token;
                                authResult.userTokenExpiresAt = currentUTCSec + refreshResp.expires_in;
                            }
                            // Update Installation
                            const botOrUser = installationUpdates[tokenType];
                            if (botOrUser !== undefined) {
                                this.logger.debug(`Saving ${tokenType} token and its refresh token in InstallationStore`);
                                botOrUser.token = refreshResp.access_token;
                                botOrUser.refreshToken = refreshResp.refresh_token;
                                botOrUser.expiresAt = currentUTCSec + refreshResp.expires_in;
                            }
                            else {
                                const errorMessage = `Unexpected data structure detected. The data returned by your InstallationStore#fetchInstallation() method must have ${tokenType} at top-level`;
                                throw new errors_1.UnknownError(errorMessage);
                            }
                        }
                        await this.installationStore.storeInstallation(installationUpdates);
                        this.logger.debug('Refreshed tokens have been saved in InstallationStore');
                    }
                    else {
                        this.logger.debug('No tokens were refreshed');
                    }
                }
            }
            return authResult;
        }
        catch (error) {
            // biome-ignore lint/suspicious/noExplicitAny: errors can be any
            throw new errors_1.AuthorizationError(error.message);
        }
        finally {
            this.logger.debug(`Completed authorize() execution (source: ${sourceForLogging})`);
        }
    }
    /**
     * refreshExpiringTokens refreshes expired access tokens using the `oauth.v2.access` endpoint.
     *
     * The return value is an Array of Promises made up of the resolution of each token refresh attempt.
     */
    async refreshExpiringTokens(tokensToRefresh) {
        const refreshPromises = tokensToRefresh.map((token) => this.refreshExpiringToken(token));
        return (await Promise.all(refreshPromises))
            .filter((res) => !(res instanceof Error))
            .map((res) => res);
    }
    async refreshExpiringToken(refreshToken) {
        return this.noTokenClient.oauth.v2
            .access({
            client_id: this.clientId,
            client_secret: this.clientSecret,
            grant_type: 'refresh_token',
            refresh_token: refreshToken,
        })
            .then((res) => res)
            .catch((e) => {
            this.logger.error(`Failed to perform oauth.v2.access API call for token rotation: (error: ${e})`);
            return e; // this one will be filtered out later
        });
    }
    // ------------------------------------------------------
    // Handling web browser requests from end-users
    // ------------------------------------------------------
    /**
     * Handles the install path (the default is /slack/install) requests from an app installer.
     */
    async handleInstallPath(req, res, options, installOptions) {
        if (installOptions === undefined && this.installUrlOptions === undefined) {
            const errorMessage = 'To enable the built-in install path handler, you need to pass InstallURLOptions to InstallProvider. ' +
                "If you're using @slack/bolt, please upgrade the framework to the latest version.";
            throw new errors_1.GenerateInstallUrlError(errorMessage);
        }
        // biome-ignore lint/style/noNonNullAssertion: TODO: we should really rewrite the logic here to drop these
        const _installOptions = installOptions || this.installUrlOptions;
        const _printableOptions = JSON.stringify(_installOptions);
        this.logger.debug(`Running handleInstallPath() with ${_printableOptions}`);
        try {
            let shouldProceed = true;
            if ((options === null || options === void 0 ? void 0 : options.beforeRedirection) !== undefined) {
                shouldProceed = await options.beforeRedirection(req, res, installOptions);
            }
            if (!shouldProceed) {
                this.logger.debug('Skipped to proceed with the built-in redirection as beforeRedirection returned false');
                return;
            }
            let state;
            if (this.stateVerification) {
                if (this.stateStore) {
                    state = await this.stateStore.generateStateParam(_installOptions, new Date());
                    const stateCookie = this.buildSetCookieHeaderForNewState(state);
                    if (res.getHeader('Set-Cookie')) {
                        // If the cookies already exist
                        const existingCookies = res.getHeader('Set-Cookie') || [];
                        const allCookies = [];
                        if (Array.isArray(existingCookies)) {
                            allCookies.push(...existingCookies);
                        }
                        else if (typeof existingCookies === 'string') {
                            allCookies.push(existingCookies);
                        }
                        else {
                            allCookies.push(existingCookies.toString());
                        }
                        // Append the state cookie
                        allCookies.push(stateCookie);
                        res.setHeader('Set-Cookie', allCookies);
                    }
                    else {
                        res.setHeader('Set-Cookie', stateCookie);
                    }
                }
                else if (this.stateStore === undefined) {
                    throw new errors_1.GenerateInstallUrlError('StateStore is not properly configured');
                }
            }
            const url = await this.generateInstallUrl(_installOptions, this.stateVerification, state);
            this.logger.debug(`Generated authorize URL: ${url}`);
            if (this.directInstall !== undefined && this.directInstall) {
                // If a Slack app sets "Direct Install URL" in the Slack app configruation,
                // the installation flow of the app should start with the Slack authorize URL.
                // See https://docs.slack.dev/slack-marketplace/distributing-your-app-in-the-slack-marketplace for more details.
                res.setHeader('Location', url);
                res.writeHead(302);
                res.end('');
            }
            else {
                // The installation starts from a landing page served by this app.
                // Generate HTML response body
                const body = this.renderHtmlForInstallPath(url);
                // Serve a basic HTML page including the "Add to Slack" button.
                // Regarding headers:
                // - Content-Length is not used because Transfer-Encoding='chunked' is automatically used.
                res.setHeader('Content-Type', 'text/html; charset=utf-8');
                res.writeHead(200);
                res.end(body);
            }
        }
        catch (e) {
            const message = `An unhandled error occurred while processing an install path request (error: ${e})`;
            this.logger.error(message);
            // biome-ignore lint/suspicious/noExplicitAny: errors can be any
            throw new errors_1.GenerateInstallUrlError(e.message);
        }
    }
    /**
     * Returns a URL that is suitable for including in an Add to Slack button
     * Uses stateStore to generate a value for the state query param.
     */
    async generateInstallUrl(options, stateVerification = true, state) {
        const slackURL = new node_url_1.URL(this.authorizationUrl);
        if (options.scopes === undefined || options.scopes === null) {
            throw new errors_1.GenerateInstallUrlError('You must provide a scope parameter when calling generateInstallUrl');
        }
        // scope
        let scopes;
        if (Array.isArray(options.scopes)) {
            scopes = options.scopes.join(',');
        }
        else {
            scopes = options.scopes;
        }
        const params = new node_url_1.URLSearchParams(`scope=${scopes}`);
        // generate state
        if (stateVerification) {
            let _state = state;
            if (_state === undefined) {
                if (this.stateStore) {
                    _state = await this.stateStore.generateStateParam(options, new Date());
                }
                else {
                    const errorMessage = 'StateStore needs to be set for generating a valid authorize URL';
                    throw new errors_1.InstallerInitializationError(errorMessage);
                }
            }
            params.append('state', _state);
        }
        // client id
        params.append('client_id', this.clientId);
        // redirect uri
        if (options.redirectUri !== undefined) {
            params.append('redirect_uri', options.redirectUri);
        }
        // team id
        if (options.teamId !== undefined) {
            params.append('team', options.teamId);
        }
        // user scope, only available for OAuth v2
        if (options.userScopes !== undefined && this.authVersion === 'v2') {
            let userScopes;
            if (Array.isArray(options.userScopes)) {
                userScopes = options.userScopes.join(',');
            }
            else {
                userScopes = options.userScopes;
            }
            params.append('user_scope', userScopes);
        }
        slackURL.search = params.toString();
        return slackURL.toString();
    }
    /**
     * This method handles the incoming request to the callback URL.
     * It can be used as a RequestListener in almost any HTTP server
     * framework.
     *
     * Verifies the state using the stateStore, exchanges the grant in the
     * query params for an access token, and stores token and associated data
     * in the installationStore.
     */
    async handleCallback(req, res, options, installOptions) {
        var _a;
        let code;
        let flowError;
        let stateInQueryString;
        try {
            if (req.url !== undefined) {
                // Note: Protocol/ host of object are not necessarily accurate
                // and shouldn't be relied on
                // intended only for accessing searchParams only
                const searchParams = extractSearchParams(req);
                flowError = searchParams.get('error');
                if (flowError === 'access_denied') {
                    throw new errors_1.AuthorizationError('User cancelled the OAuth installation flow!');
                }
                code = searchParams.get('code');
                stateInQueryString = searchParams.get('state');
                if (!code) {
                    throw new errors_1.MissingCodeError('Redirect url is missing the required code query parameter');
                }
                if (this.stateVerification && !stateInQueryString) {
                    throw new errors_1.MissingStateError('Redirect url is missing the state query parameter. If this is intentional, see options for disabling default state verification.');
                }
            }
            else {
                throw new errors_1.UnknownError('Something went wrong');
            }
            // If state verification is enabled, attempt to verify, otherwise ignore
            if (this.stateVerification) {
                try {
                    if (this.legacyStateVerification) {
                        // This mode is not enabled by default
                        // This option is for some of the existing developers that need time for migration
                        this.logger.warn('Enabling legacyStateVerification is not recommended as it does not properly work for OAuth CSRF protection. Please consider migrating from directly using InstallProvider#generateInstallUrl() to InstallProvider#handleInstallPath() for serving the install path.');
                    }
                    else {
                        const stateInBrowserSession = extractCookieValue(req, this.stateCookieName);
                        if (!stateInBrowserSession || stateInBrowserSession !== stateInQueryString) {
                            throw new errors_1.InvalidStateError('The state parameter is not for this browser session.');
                        }
                    }
                    if (this.stateStore) {
                        // biome-ignore lint/style/noParameterAssign: we reassigning, deal with it
                        installOptions = await this.stateStore.verifyStateParam(new Date(), stateInQueryString);
                    }
                    else {
                        throw new errors_1.InstallerInitializationError('StateStore is not properly configured');
                    }
                }
                finally {
                    // Delete the state value in cookies in any case
                    res.setHeader('Set-Cookie', this.buildSetCookieHeaderForStateDeletion());
                }
            }
            if (!installOptions) {
                const emptyInstallOptions = { scopes: [] };
                // biome-ignore lint/style/noParameterAssign: we reassigning, deal with it
                installOptions = emptyInstallOptions;
            }
            // beforeInstallation/afterInstallation may return false
            let shouldProceed = true;
            if ((options === null || options === void 0 ? void 0 : options.beforeInstallation) !== undefined) {
                shouldProceed = await options.beforeInstallation(installOptions, req, res);
            }
            if (!shouldProceed) {
                // When options.beforeInstallation returns false,
                // the app installation is cancelled
                // The beforeInstallation method is responsible for building a complete HTTP response.
                return;
            }
            // Start: Build the installation object
            let installation;
            let resp;
            if (this.authVersion === 'v1') {
                // convert response type from WebApiCallResult to OAuthResponse
                const v1Resp = (await this.noTokenClient.oauth.access({
                    code,
                    client_id: this.clientId,
                    client_secret: this.clientSecret,
                    redirect_uri: installOptions.redirectUri,
                }));
                // resp obj for v1 - https://docs.slack.dev/reference/methods/oauth.access#response
                const v1Installation = {
                    team: { id: v1Resp.team_id, name: v1Resp.team_name },
                    enterprise: v1Resp.enterprise_id === null ? undefined : { id: v1Resp.enterprise_id },
                    user: {
                        token: v1Resp.access_token,
                        scopes: v1Resp.scope.split(','),
                        id: v1Resp.user_id,
                    },
                    // synthesized properties: enterprise installation is unsupported in v1 auth
                    isEnterpriseInstall: false,
                    authVersion: 'v1',
                };
                // only can get botId if bot access token exists
                // need to create a botUser + request bot scope to have this be part of resp
                if (v1Resp.bot !== undefined) {
                    const authResult = await runAuthTest(v1Resp.bot.bot_access_token, this.clientOptions);
                    // We already tested that a bot user was in the response, so we know the following bot_id will be defined
                    const botId = authResult.bot_id;
                    v1Installation.bot = {
                        id: botId,
                        scopes: ['bot'],
                        token: v1Resp.bot.bot_access_token,
                        userId: v1Resp.bot.bot_user_id,
                    };
                }
                resp = v1Resp;
                installation = v1Installation;
            }
            else {
                // convert response type from WebApiCallResult to OAuthResponse
                const v2Resp = (await this.noTokenClient.oauth.v2.access({
                    code,
                    client_id: this.clientId,
                    client_secret: this.clientSecret,
                    redirect_uri: installOptions.redirectUri,
                }));
                // resp obj for v2 - https://docs.slack.dev/reference/methods/oauth.v2.access#response
                const v2Installation = {
                    team: v2Resp.team === null ? undefined : v2Resp.team,
                    enterprise: v2Resp.enterprise == null ? undefined : v2Resp.enterprise,
                    user: {
                        token: v2Resp.authed_user.access_token,
                        scopes: (_a = v2Resp.authed_user.scope) === null || _a === void 0 ? void 0 : _a.split(','),
                        id: v2Resp.authed_user.id,
                    },
                    tokenType: v2Resp.token_type,
                    isEnterpriseInstall: v2Resp.is_enterprise_install,
                    appId: v2Resp.app_id,
                    // synthesized properties
                    authVersion: 'v2',
                };
                const currentUTC = Math.floor(Date.now() / 1000); // utc, seconds
                // Installation has Bot Token
                if (v2Resp.access_token !== undefined && v2Resp.scope !== undefined && v2Resp.bot_user_id !== undefined) {
                    const authResult = await runAuthTest(v2Resp.access_token, this.clientOptions);
                    v2Installation.bot = {
                        scopes: v2Resp.scope.split(','),
                        token: v2Resp.access_token,
                        userId: v2Resp.bot_user_id,
                        id: authResult.bot_id,
                    };
                    if (v2Resp.is_enterprise_install) {
                        v2Installation.enterpriseUrl = authResult.url;
                    }
                    // Token Rotation is Enabled
                    if (v2Resp.refresh_token !== undefined && v2Resp.expires_in !== undefined) {
                        v2Installation.bot.refreshToken = v2Resp.refresh_token;
                        v2Installation.bot.expiresAt = currentUTC + v2Resp.expires_in; // utc, seconds
                    }
                }
                // Installation has User Token
                if (v2Resp.authed_user !== undefined && v2Resp.authed_user.access_token !== undefined) {
                    if (v2Resp.is_enterprise_install && v2Installation.enterpriseUrl === undefined) {
                        const authResult = await runAuthTest(v2Resp.authed_user.access_token, this.clientOptions);
                        v2Installation.enterpriseUrl = authResult.url;
                    }
                    // Token Rotation is Enabled
                    if (v2Resp.authed_user.refresh_token !== undefined && v2Resp.authed_user.expires_in !== undefined) {
                        v2Installation.user.refreshToken = v2Resp.authed_user.refresh_token;
                        v2Installation.user.expiresAt = currentUTC + v2Resp.authed_user.expires_in; // utc, seconds
                    }
                }
                resp = v2Resp;
                installation = v2Installation;
            }
            if (resp.incoming_webhook !== undefined) {
                installation.incomingWebhook = {
                    url: resp.incoming_webhook.url,
                    channel: resp.incoming_webhook.channel,
                    channelId: resp.incoming_webhook.channel_id,
                    configurationUrl: resp.incoming_webhook.configuration_url,
                };
            }
            if (installOptions && installOptions.metadata !== undefined) {
                // Pass the metadata in state parameter if exists.
                // Developers can use the value for additional/custom data associated with the installation.
                installation.metadata = installOptions.metadata;
            }
            // End: Build the installation object
            if ((options === null || options === void 0 ? void 0 : options.afterInstallation) !== undefined) {
                shouldProceed = await options.afterInstallation(installation, installOptions, req, res);
            }
            if (!shouldProceed) {
                // When options.beforeInstallation returns false,
                // the app installation is cancelled
                // The afterInstallation method is responsible for building a complete HTTP response.
                return;
            }
            // Save installation object to installation store
            await this.installationStore.storeInstallation(installation, this.logger);
            // Call the success callback
            if (options !== undefined && (options.success !== undefined || options.successAsync !== undefined)) {
                if (options.success !== undefined) {
                    this.logger.debug('Calling passed function as callbackOptions.success');
                    options.success(installation, installOptions, req, res);
                }
                if (options.successAsync !== undefined) {
                    this.logger.debug('Calling passed function as callbackOptions.successAsync');
                    await options.successAsync(installation, installOptions, req, res);
                }
            }
            else {
                this.logger.debug('Running built-in success function');
                (0, callback_options_1.defaultCallbackSuccess)(installation, installOptions, req, res);
            }
        }
        catch (error) {
            this.logger.error(error);
            if (!installOptions) {
                // To make the `installOptions` type compatible with `CallbackOptions#failure` signature
                const emptyInstallOptions = { scopes: [] };
                // biome-ignore lint/style/noParameterAssign: we reassigning, deal with it
                installOptions = emptyInstallOptions;
            }
            // Call the failure callback
            const codedError = error;
            if (codedError.code === undefined) {
                codedError.code = errors_1.ErrorCode.UnknownError;
            }
            if (options !== undefined && (options.failure !== undefined || options.failureAsync !== undefined)) {
                if (options.failure !== undefined) {
                    this.logger.debug('Calling passed function as callbackOptions.failure');
                    options.failure(codedError, installOptions, req, res);
                }
                if (options.failureAsync !== undefined) {
                    this.logger.debug('Calling passed function as callbackOptions.failureAsync');
                    await options.failureAsync(codedError, installOptions, req, res);
                }
            }
            else {
                this.logger.debug('Running built-in failure function');
                (0, callback_options_1.defaultCallbackFailure)(codedError, installOptions, req, res);
            }
        }
    }
    // -----------------------
    // Internal methods
    buildSetCookieHeaderForNewState(state) {
        const name = this.stateCookieName;
        const maxAge = this.stateCookieExpirationSeconds;
        return `${name}=${state}; Secure; HttpOnly; Path=/; Max-Age=${maxAge}`;
    }
    buildSetCookieHeaderForStateDeletion() {
        const name = this.stateCookieName;
        return `${name}=deleted; Secure; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
    }
}
exports.InstallProvider = InstallProvider;
async function runAuthTest(token, clientOptions) {
    const client = new web_api_1.WebClient(token, clientOptions);
    const authResult = await client.auth.test({});
    return authResult;
}
/**
 * detectExpiredOrExpiringTokens determines access tokens' eligibility for refresh.
 *
 * The return value is an Array of expired or soon-to-expire access tokens.
 */
function detectExpiredOrExpiringTokens(authResult, currentUTCSec) {
    const tokensToRefresh = [];
    const EXPIRY_WINDOW = 7200; // 2 hours
    if (authResult.botRefreshToken &&
        authResult.botTokenExpiresAt !== undefined &&
        authResult.botTokenExpiresAt !== null) {
        const botTokenExpiresIn = authResult.botTokenExpiresAt - currentUTCSec;
        if (botTokenExpiresIn <= EXPIRY_WINDOW) {
            tokensToRefresh.push(authResult.botRefreshToken);
        }
    }
    if (authResult.userRefreshToken &&
        authResult.userTokenExpiresAt !== undefined &&
        authResult.userTokenExpiresAt !== null) {
        const userTokenExpiresIn = authResult.userTokenExpiresAt - currentUTCSec;
        if (userTokenExpiresIn <= EXPIRY_WINDOW) {
            tokensToRefresh.push(authResult.userRefreshToken);
        }
    }
    return tokensToRefresh;
}
/**
 * Returns search params from a URL and ignores protocol / hostname as those
 * aren't guaranteed to be accurate e.g. in x-forwarded- scenarios
 */
function extractSearchParams(req) {
    const { searchParams } = new node_url_1.URL(req.url, `https://${req.headers.host}`);
    return searchParams;
}
function extractCookieValue(req, name) {
    const allCookies = req.headers.cookie;
    if (allCookies) {
        const found = allCookies.split(';').find((c) => c.trim().startsWith(`${name}=`));
        if (found) {
            return found.split('=')[1].trim();
        }
    }
    return undefined;
}
//# sourceMappingURL=install-provider.js.map