UNPKG

@slack/oauth

Version:

Official library for interacting with Slack's Oauth endpoints

663 lines 35.6 kB
"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