@slack/oauth
Version:
Official library for interacting with Slack's Oauth endpoints
663 lines • 35.6 kB
JavaScript
"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