slack-edge
Version:
Slack app development framework for edge functions with streamlined TypeScript support
386 lines • 16.2 kB
JavaScript
"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