UNPKG

slack-edge

Version:

Slack app development framework for edge functions with streamlined TypeScript support

386 lines 16.2 kB
"use strict"; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _SlackOAuthApp_instances, _SlackOAuthApp_enableTokenRevocationHandlers, _SlackOAuthApp_validateStateParameter; Object.defineProperty(exports, "__esModule", { value: true }); exports.SlackOAuthApp = void 0; const execution_context_1 = require("./execution-context"); const app_1 = require("./app"); const state_store_1 = require("./oauth/state-store"); const authorize_url_generator_1 = require("./oauth/authorize-url-generator"); const cookie_1 = require("./cookie"); const slack_web_api_client_1 = require("slack-web-api-client"); const installation_1 = require("./oauth/installation"); const hook_1 = require("./oauth/hook"); const hook_2 = require("./oidc/hook"); const authorize_url_generator_2 = require("./oidc/authorize-url-generator"); const error_codes_1 = require("./oauth/error-codes"); /** * The class representing a Slack app process * that handles both event requests and the OAuth flow for app installation. */ class SlackOAuthApp extends app_1.SlackApp { constructor(options) { super({ env: options.env, authorize: options.installationStore.toAuthorize(), authorizeErrorHandler: options.authorizeErrorHandler, routes: { events: options.routes?.events ?? "/slack/events" }, startLazyListenerAfterAck: options.startLazyListenerAfterAck, ignoreSelfEvents: options.ignoreSelfEvents, assistantThreadContextStore: options.assistantThreadContextStore, }); _SlackOAuthApp_instances.add(this); this.env = options.env; this.installationStore = options.installationStore; this.stateStore = options.stateStore ?? new state_store_1.NoStorageStateStore(); this.oauth = { stateCookieName: options.oauth?.stateCookieName ?? "slack-app-oauth-state", onFailure: options.oauth?.onFailure ?? (0, hook_1.defaultOnFailure)(options.oauth?.onFailureRenderer), onStateValidationError: options.oauth?.onStateValidationError ?? (0, hook_1.defaultOnStateValidationError)(options.oauth?.onStateValidationRenderer), redirectUri: options.oauth?.redirectUri ?? this.env.SLACK_REDIRECT_URI, start: options.oauth?.start ?? (0, hook_1.defaultOAuthStart)(options.oauth?.startImmediateRedirect, options.oauth?.startRenderer), beforeInstallation: options.oauth?.beforeInstallation, afterInstallation: options.oauth?.afterInstallation, callback: options.oauth?.callback ?? (0, hook_1.defaultOAuthCallback)(options.oauth?.callbackRenderer), }; if (options.oidc) { this.oidc = { stateCookieName: options.oidc.stateCookieName ?? "slack-app-oidc-state", onFailure: options.oidc.onFailure ?? (0, hook_1.defaultOnFailure)(options.oidc?.onFailureRenderer), onStateValidationError: options.oidc.onStateValidationError ?? (0, hook_1.defaultOnStateValidationError)(options.oidc?.onStateValidationRenderer), start: options.oidc?.start ?? (0, hook_1.defaultOAuthStart)(options.oidc?.startImmediateRedirect, options.oidc?.startRenderer), callback: options.oidc.callback ?? hook_2.defaultOpenIDConnectCallback, redirectUri: options.oidc.redirectUri ?? this.env.SLACK_OIDC_REDIRECT_URI, }; } else { this.oidc = undefined; } this.routes = options.routes ? options.routes : { events: "/slack/events", oauth: { start: "/slack/install", callback: "/slack/oauth_redirect", }, oidc: { start: "/slack/login", callback: "/slack/login/callback", }, }; __classPrivateFieldGet(this, _SlackOAuthApp_instances, "m", _SlackOAuthApp_enableTokenRevocationHandlers).call(this, options.installationStore); } async run(request, ctx = new execution_context_1.NoopExecutionContext()) { const url = new URL(request.url); if (request.method === "GET") { if (url.pathname === this.routes.oauth.start) { return await this.handleOAuthStartRequest(request); } else if (url.pathname === this.routes.oauth.callback) { return await this.handleOAuthCallbackRequest(request); } if (this.routes.oidc) { if (url.pathname === this.routes.oidc.start) { return await this.handleOIDCStartRequest(request); } else if (url.pathname === this.routes.oidc.callback) { return await this.handleOIDCCallbackRequest(request); } } } else if (request.method === "POST") { if (url.pathname === this.routes.events) { return await this.handleEventRequest(request, ctx); } } return new Response("Not found", { status: 404 }); } /** * Handles an HTTP request from Slack's API server and returns a response to it. * @param request request * @param ctx execution context * @returns response */ async handleEventRequest(request, ctx) { return await super.handleEventRequest(request, ctx); } /** * Handles an HTTP request to initiate the app-installation OAuth flow within a web browser. * @param request request * @returns response */ async handleOAuthStartRequest(request) { const stateValue = await this.stateStore.issueNewState(); const url = new URL(request.url); const team = url.searchParams.get("team") || undefined; const authorizeUrl = (0, authorize_url_generator_1.generateAuthorizeUrl)(stateValue, this.env, team); return await this.oauth.start({ env: this.env, authorizeUrl, stateCookieName: this.oauth.stateCookieName, stateValue, request, }); } /** * Handles an HTTP request to handle the app-installation OAuth flow callback within a web browser. * @param request request * @returns response */ async handleOAuthCallbackRequest(request) { // State parameter validation const errorResponse = await __classPrivateFieldGet(this, _SlackOAuthApp_instances, "m", _SlackOAuthApp_validateStateParameter).call(this, request, this.routes.oauth.start, this.oauth.stateCookieName); if (errorResponse) { return errorResponse; } const { searchParams } = new URL(request.url); const error = searchParams.get("error"); if (!error && error !== null) { return await this.oauth.onFailure({ env: this.env, startPath: this.routes.oauth.start, reason: { code: error, message: `The installation process failed due to "${error}"` }, request, }); } const code = searchParams.get("code"); if (!code) { return await this.oauth.onFailure({ env: this.env, startPath: this.routes.oauth.start, reason: error_codes_1.MissingCode, request, }); } if (this.oauth.beforeInstallation) { const response = await this.oauth.beforeInstallation({ env: this.env, request, }); if (response) { return response; } } const client = new slack_web_api_client_1.SlackAPIClient(undefined, { logLevel: this.env.SLACK_LOGGING_LEVEL, }); let oauthAccess; try { // Execute the installation process oauthAccess = await client.oauth.v2.access({ client_id: this.env.SLACK_CLIENT_ID, client_secret: this.env.SLACK_CLIENT_SECRET, redirect_uri: this.oauth.redirectUri, code, }); } catch (e) { console.log(e); return await this.oauth.onFailure({ env: this.env, startPath: this.routes.oauth.start, reason: error_codes_1.InstallationError, request, }); } const installation = (0, installation_1.toInstallation)(oauthAccess); if (this.oauth.afterInstallation) { const response = await this.oauth.afterInstallation({ env: this.env, request, installation, }); if (response) { return response; } } try { // Store the installation data on this app side await this.installationStore.save(installation, request); } catch (e) { console.log(e); return await this.oauth.onFailure({ env: this.env, startPath: this.routes.oauth.start, reason: error_codes_1.InstallationStoreError, request, }); } try { // Build the completion page const authTestResponse = await client.auth.test({ token: oauthAccess.access_token, }); const enterpriseUrl = authTestResponse.url; return await this.oauth.callback({ env: this.env, oauthAccess, enterpriseUrl, stateCookieName: this.oauth.stateCookieName, installation, authTestResponse, request, }); } catch (e) { console.log(e); return await this.oauth.onFailure({ env: this.env, startPath: this.routes.oauth.start, reason: error_codes_1.CompletionPageError, request, }); } } /** * Handles an HTTP request to initiate the SIWS flow within a web browser. * @param request request * @returns response */ async handleOIDCStartRequest(request) { if (!this.oidc) { return new Response("Not found", { status: 404 }); } const stateValue = await this.stateStore.issueNewState(); const authorizeUrl = (0, authorize_url_generator_2.generateOIDCAuthorizeUrl)(stateValue, this.env); return await this.oidc.start({ env: this.env, authorizeUrl, stateCookieName: this.oidc.stateCookieName, stateValue, request, }); } /** * Handles an HTTP request to handle the SIWS callback within a web browser. * @param request request * @returns response */ async handleOIDCCallbackRequest(request) { if (!this.oidc || !this.routes.oidc) { return new Response("Not found", { status: 404 }); } // State parameter validation const errorResponse = await __classPrivateFieldGet(this, _SlackOAuthApp_instances, "m", _SlackOAuthApp_validateStateParameter).call(this, request, this.routes.oidc.start, this.oidc.stateCookieName); if (errorResponse) { return errorResponse; } const { searchParams } = new URL(request.url); const code = searchParams.get("code"); if (!code) { return await this.oidc.onFailure({ env: this.env, startPath: this.routes.oidc.start, reason: error_codes_1.MissingCode, request, }); } try { const client = new slack_web_api_client_1.SlackAPIClient(undefined, { logLevel: this.env.SLACK_LOGGING_LEVEL, }); const token = await client.openid.connect.token({ client_id: this.env.SLACK_CLIENT_ID, client_secret: this.env.SLACK_CLIENT_SECRET, redirect_uri: this.oidc.redirectUri, code, }); return await this.oidc.callback({ env: this.env, token, request, }); } catch (e) { console.log(e); return await this.oidc.onFailure({ env: this.env, startPath: this.routes.oidc.start, reason: error_codes_1.OpenIDConnectError, request, }); } } } exports.SlackOAuthApp = SlackOAuthApp; _SlackOAuthApp_instances = new WeakSet(), _SlackOAuthApp_enableTokenRevocationHandlers = function _SlackOAuthApp_enableTokenRevocationHandlers(installationStore) { this.event("tokens_revoked", async ({ payload, body }) => { if (Array.isArray(payload.tokens.bot) && payload.tokens.bot.length > 0) { // actually only one bot per app in a workspace try { await installationStore.deleteBotInstallation({ enterpriseId: body.enterprise_id, teamId: body.team_id, }); } catch (e) { console.log(`Failed to delete a bot installation (error: ${e})`); } } if (Array.isArray(payload.tokens.oauth) && payload.tokens.oauth.length > 0) { for (const userId of payload.tokens.oauth) { try { await installationStore.deleteUserInstallation({ enterpriseId: body.enterprise_id, teamId: body.team_id, userId, }); } catch (e) { console.log(`Failed to delete a user installation (error: ${e})`); } } } }); this.event("app_uninstalled", async ({ body }) => { try { await installationStore.deleteAll({ enterpriseId: body.enterprise_id, teamId: body.team_id, }); } catch (e) { console.log(`Failed to delete all installation for an app_uninstalled event (error: ${e})`); } }); this.event("app_uninstalled_team", async ({ body }) => { try { await installationStore.deleteAll({ enterpriseId: body.enterprise_id, teamId: body.team_id, }); } catch (e) { console.log(`Failed to delete all installation for an app_uninstalled_team event (error: ${e})`); } }); }, _SlackOAuthApp_validateStateParameter = async function _SlackOAuthApp_validateStateParameter(request, startPath, cookieName) { const { searchParams } = new URL(request.url); const queryState = searchParams.get("state"); const cookie = (0, cookie_1.parse)(request.headers.get("Cookie") || ""); const cookieState = cookie[cookieName]; if (queryState !== cookieState || !(await this.stateStore.consume(queryState))) { if (startPath === this.routes.oauth.start) { return await this.oauth.onStateValidationError({ env: this.env, startPath, request, }); } else if (this.oidc && this.routes.oidc && startPath === this.routes.oidc.start) { return await this.oidc.onStateValidationError({ env: this.env, startPath, request, }); } } return undefined; }; //# sourceMappingURL=oauth-app.js.map