@slack/bolt
Version:
A framework for building Slack apps, fast.
225 lines • 10 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_crypto_1 = __importDefault(require("node:crypto"));
const node_querystring_1 = __importDefault(require("node:querystring"));
const logger_1 = require("@slack/logger");
const tsscmp_1 = __importDefault(require("tsscmp"));
const errors_1 = require("../errors");
/*
* Receiver implementation for AWS API Gateway + Lambda apps
*
* Note that this receiver does not support Slack OAuth flow.
* For OAuth flow endpoints, deploy another Lambda function built with ExpressReceiver.
*/
class AwsLambdaReceiver {
signingSecret;
app;
_logger;
get logger() {
return this._logger;
}
signatureVerification;
customPropertiesExtractor;
invalidRequestSignatureHandler;
unhandledRequestTimeoutMillis;
constructor({ signingSecret, logger = undefined, logLevel = logger_1.LogLevel.INFO, signatureVerification = true, customPropertiesExtractor = (_) => ({}), invalidRequestSignatureHandler, unhandledRequestTimeoutMillis = 3001, }) {
// Initialize instance variables, substituting defaults for each value
this.signingSecret = signingSecret;
this.signatureVerification = signatureVerification;
this.unhandledRequestTimeoutMillis = unhandledRequestTimeoutMillis;
this._logger =
logger ??
(() => {
const defaultLogger = new logger_1.ConsoleLogger();
defaultLogger.setLevel(logLevel);
return defaultLogger;
})();
this.customPropertiesExtractor = customPropertiesExtractor;
if (invalidRequestSignatureHandler) {
this.invalidRequestSignatureHandler = invalidRequestSignatureHandler;
}
else {
this.invalidRequestSignatureHandler = this.defaultInvalidRequestSignatureHandler;
}
}
init(app) {
this.app = app;
}
// biome-ignore lint/suspicious/noExplicitAny: TODO: what should the REceiver interface here be? probably needs work
start(..._args) {
return new Promise((resolve, reject) => {
try {
const handler = this.toHandler();
resolve(handler);
}
catch (error) {
reject(error);
}
});
}
// biome-ignore lint/suspicious/noExplicitAny: TODO: what should the REceiver interface here be? probably needs work
stop(..._args) {
return new Promise((resolve, _reject) => {
resolve();
});
}
toHandler() {
// biome-ignore lint/suspicious/noExplicitAny: request context can be anything
return async (awsEvent, _awsContext, _awsCallback) => {
this.logger.debug(`AWS event: ${JSON.stringify(awsEvent, null, 2)}`);
const rawBody = this.getRawBody(awsEvent);
// biome-ignore lint/suspicious/noExplicitAny: request bodies can be anything
const body = this.parseRequestBody(rawBody, this.getHeaderValue(awsEvent.headers, 'Content-Type'), this.logger);
// ssl_check (for Slash Commands)
if (typeof body !== 'undefined' &&
body != null &&
typeof body.ssl_check !== 'undefined' &&
body.ssl_check != null) {
return Promise.resolve({ statusCode: 200, body: '' });
}
if (this.signatureVerification) {
// request signature verification
const signature = this.getHeaderValue(awsEvent.headers, 'X-Slack-Signature');
const ts = Number(this.getHeaderValue(awsEvent.headers, 'X-Slack-Request-Timestamp'));
if (!this.isValidRequestSignature(this.signingSecret, rawBody, signature, ts)) {
const awsResponse = Promise.resolve({ statusCode: 401, body: '' });
this.invalidRequestSignatureHandler({ rawBody, signature, ts, awsEvent, awsResponse });
return awsResponse;
}
}
// url_verification (Events API)
if (typeof body !== 'undefined' &&
body != null &&
typeof body.type !== 'undefined' &&
body.type != null &&
body.type === 'url_verification') {
return Promise.resolve({
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ challenge: body.challenge }),
});
}
// Setup ack timeout warning
let isAcknowledged = false;
const noAckTimeoutId = setTimeout(() => {
if (!isAcknowledged) {
this.logger.error(`An incoming event was not acknowledged within ${this.unhandledRequestTimeoutMillis} ms. Ensure that the ack() argument is called in a listener.`);
}
}, this.unhandledRequestTimeoutMillis);
// Structure the ReceiverEvent
// biome-ignore lint/suspicious/noExplicitAny: request responses can be anything
let storedResponse;
const event = {
body,
ack: async (response) => {
if (isAcknowledged) {
throw new errors_1.ReceiverMultipleAckError();
}
isAcknowledged = true;
clearTimeout(noAckTimeoutId);
if (typeof response === 'undefined' || response == null) {
storedResponse = '';
}
else {
storedResponse = response;
}
},
retryNum: this.getHeaderValue(awsEvent.headers, 'X-Slack-Retry-Num'),
retryReason: this.getHeaderValue(awsEvent.headers, 'X-Slack-Retry-Reason'),
customProperties: this.customPropertiesExtractor(awsEvent),
};
// Send the event to the app for processing
try {
await this.app?.processEvent(event);
if (storedResponse !== undefined) {
if (typeof storedResponse === 'string') {
return { statusCode: 200, body: storedResponse };
}
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(storedResponse),
};
}
}
catch (err) {
this.logger.error('An unhandled error occurred while Bolt processed an event');
this.logger.debug(`Error details: ${err}, storedResponse: ${storedResponse}`);
return { statusCode: 500, body: 'Internal server error' };
}
// No matching handler; clear ack warning timeout and return a 404.
clearTimeout(noAckTimeoutId);
let path;
if ('path' in awsEvent) {
path = awsEvent.path;
}
else {
path = awsEvent.rawPath;
}
this.logger.info(`No request handler matched the request: ${path}`);
return { statusCode: 404, body: '' };
};
}
getRawBody(awsEvent) {
if (typeof awsEvent.body === 'undefined' || awsEvent.body == null) {
return '';
}
if (awsEvent.isBase64Encoded) {
return Buffer.from(awsEvent.body, 'base64').toString('ascii');
}
return awsEvent.body;
}
// biome-ignore lint/suspicious/noExplicitAny: request bodies can be anything
parseRequestBody(stringBody, contentType, logger) {
if (contentType === 'application/x-www-form-urlencoded') {
const parsedBody = node_querystring_1.default.parse(stringBody);
if (typeof parsedBody.payload === 'string') {
return JSON.parse(parsedBody.payload);
}
return parsedBody;
}
if (contentType === 'application/json') {
return JSON.parse(stringBody);
}
logger.warn(`Unexpected content-type detected: ${contentType}`);
try {
// Parse this body anyway
return JSON.parse(stringBody);
}
catch (e) {
logger.error(`Failed to parse body as JSON data for content-type: ${contentType}`);
throw e;
}
}
isValidRequestSignature(signingSecret, body, signature, requestTimestamp) {
if (!signature || !requestTimestamp) {
return false;
}
// Divide current date to match Slack ts format
// Subtract 5 minutes from current time
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5;
if (requestTimestamp < fiveMinutesAgo) {
return false;
}
const hmac = node_crypto_1.default.createHmac('sha256', signingSecret);
const [version, hash] = signature.split('=');
hmac.update(`${version}:${requestTimestamp}:${body}`);
if (!(0, tsscmp_1.default)(hash, hmac.digest('hex'))) {
return false;
}
return true;
}
getHeaderValue(headers, key) {
const caseInsensitiveKey = Object.keys(headers).find((it) => key.toLowerCase() === it.toLowerCase());
return caseInsensitiveKey !== undefined ? headers[caseInsensitiveKey] : undefined;
}
defaultInvalidRequestSignatureHandler(args) {
const { signature, ts } = args;
this.logger.info(`Invalid request signature detected (X-Slack-Signature: ${signature}, X-Slack-Request-Timestamp: ${ts})`);
}
}
exports.default = AwsLambdaReceiver;
//# sourceMappingURL=AwsLambdaReceiver.js.map