UNPKG

@wisegpt/awscdk-slack-event-bus

Version:

Exposes a Slack Events API Request URL that validates and sends all received events to an AWS Event Bus

353 lines (340 loc) 11.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/lambdas/slack-handler.lambda.ts var slack_handler_lambda_exports = {}; __export(slack_handler_lambda_exports, { SlackHandlerLambda: () => SlackHandlerLambda, handler: () => handler }); module.exports = __toCommonJS(slack_handler_lambda_exports); // src/lambdas/base-lambda.ts var BaseLambda = class { async execute(event, context) { try { return await this.handle(event, context); } catch (err) { console.log(JSON.stringify({ event, context })); console.error( JSON.stringify({ message: err?.message, err: JSON.stringify(err, Object.getOwnPropertyNames(err)) }) ); return { statusCode: 500, headers: { "content-type": "application/json" }, body: JSON.stringify({ message: "internal error occurred. please check logs." }) }; } } }; var createLambdaHandler = (lambda) => async (event, context) => lambda.execute(event, context); // src/internal/event-bus/event-bus-adapter.ts var import_client_eventbridge = require("@aws-sdk/client-eventbridge"); var EventBusAdapter = class { constructor(eventBusName, eventBridgeClient = new import_client_eventbridge.EventBridgeClient({})) { this.eventBusName = eventBusName; this.eventBridgeClient = eventBridgeClient; } async send(input) { await this.eventBridgeClient.send( new import_client_eventbridge.PutEventsCommand({ Entries: [ { EventBusName: this.eventBusName, Time: input.time, Source: input.source, DetailType: input.detailType, Detail: JSON.stringify(input.detail) } ] }) ); } }; // src/internal/event-bus/slack-event-bus.service.ts function assertSlackEventPayloadUnreachable(payload) { throw new Error( `unknown slack event type encountered: ${payload.type}` ); } var _SlackEventBusService = class { constructor(eventBusAdapter = new EventBusAdapter( _SlackEventBusService.SLACK_EVENT_BUS_NAME )) { this.eventBusAdapter = eventBusAdapter; } async send({ payload, time }) { switch (payload.type) { case "app_rate_limited": return this.eventBusAdapter.send({ detail: payload, detailType: "AppRateLimited", source: _SlackEventBusService.SLACK_EVENT_SOURCE, time }); case "event_callback": return this.eventBusAdapter.send({ detail: payload, detailType: `EventCallback.${payload.event.type}`, source: _SlackEventBusService.SLACK_EVENT_SOURCE, time }); case "url_verification": { return; } default: assertSlackEventPayloadUnreachable(payload); } } }; var SlackEventBusService = _SlackEventBusService; SlackEventBusService.SLACK_EVENT_BUS_NAME = process.env.SLACK_EVENT_BUS_NAME; SlackEventBusService.SLACK_EVENT_SOURCE = "com.slack"; // src/internal/slack/slack-parse-event.ts function slackParseEvent(rawBody) { const event = JSON.parse(rawBody); switch (event.type) { case "url_verification": { const payload = { type: "url_verification", challenge: event.challenge }; return { payload, time: /* @__PURE__ */ new Date() }; } case "app_rate_limited": { const payload = { ...event, type: "app_rate_limited", token: void 0 }; return { payload, time: new Date(payload.minute_rate_limited * 1e3) }; } case "event_callback": { const payload = { ...event, type: "event_callback", token: void 0 }; return { payload, time: new Date(payload.event_time * 1e3) }; } default: throw new Error(`could not parse unknown event type ${event.type}`); } } // src/internal/slack/slack-verify-signature.ts var import_crypto = require("crypto"); function slackVerifySignature({ signingSecret, rawBody, requestTimestampHeader, signatureHeader }) { if (!signatureHeader) { throw new Error("slack signature header is not present"); } if (!requestTimestampHeader) { throw new Error("request timestamp header is not present"); } const [version, signature] = signatureHeader.split("="); if (version !== "v0" || !signature) { throw new Error("invalid slack signature format"); } try { const receivedSignatureBuffer = Buffer.from(signature, "hex"); const baseString = `${version}:${requestTimestampHeader}:${rawBody}`; const calculatedSignatureBuffer = Buffer.from( (0, import_crypto.createHmac)("sha256", signingSecret).update(baseString).digest().toString("hex"), "hex" ); return (0, import_crypto.timingSafeEqual)(calculatedSignatureBuffer, receivedSignatureBuffer); } catch { return false; } } // src/internal/secrets/secrets-manager-adapter.ts var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager"); var SecretsManagerAdapter = class { constructor(secretArn, secretsManagerClient = new import_client_secrets_manager.SecretsManagerClient({})) { this.secretArn = secretArn; this.secretsManagerClient = secretsManagerClient; } async retrieve() { const result = await this.secretsManagerClient.send( new import_client_secrets_manager.GetSecretValueCommand({ SecretId: this.secretArn }) ); return JSON.parse(result.SecretString); } }; // src/internal/secrets/slack-secrets.service.ts var _SlackSecretsService = class { constructor(secretsManagerAdapter = new SecretsManagerAdapter( _SlackSecretsService.SLACK_SECRET_ARN )) { this.secretsManagerAdapter = secretsManagerAdapter; } async retrieve() { const currentTime = Date.now(); if (this.cache && // cache not expired Date.now() - this.cache.time < _SlackSecretsService.SLACK_SECRET_TTL) { return this.cache.promise; } this.cache = { promise: this.retrieveAndParseSecret(), time: currentTime }; return this.cache.promise; } async retrieveAndParseSecret() { const secretObject = await this.secretsManagerAdapter.retrieve(); return Object.entries(secretObject).reduce( (curr, [key, value]) => { const match = _SlackSecretsService.SIGNING_SECRET_REGEX.exec(key); if (match && match.groups) { const { appId } = match.groups; curr[appId] = { signingSecret: value }; } return curr; }, {} ); } }; var SlackSecretsService = _SlackSecretsService; SlackSecretsService.SLACK_SECRET_ARN = process.env.SLACK_SECRET_ARN; SlackSecretsService.SIGNING_SECRET_REGEX = /^app\/(?<appId>[a-zA-Z0-9]+)\/signing-secret$/; SlackSecretsService.SLACK_SECRET_TTL = 60 * 1e3; // src/internal/slack/slack.service.ts var SlackService = class { constructor(slackSecretsService = new SlackSecretsService()) { this.slackSecretsService = slackSecretsService; } async verifySignature(input) { const appSecretsMap = await this.slackSecretsService.retrieve(); const appSecrets = appSecretsMap[input.appId]; if (appSecrets === void 0) { throw new Error( `unknown appId '${input.appId}', insert signing-secret for the given app` ); } return slackVerifySignature({ signingSecret: appSecrets.signingSecret, rawBody: input.body, requestTimestampHeader: input.headers["x-slack-request-timestamp"], signatureHeader: input.headers["x-slack-signature"] }); } parseEvent(rawBody) { return slackParseEvent(rawBody); } }; // src/internal/handlers/slack-event-handler.service.ts function assertSlackEventPayloadUnreachable2(payload) { throw new Error( `unknown slack event type encountered: ${payload.type}` ); } var SlackEventHandlerService = class { constructor(slackEventBusService = new SlackEventBusService(), slackService = new SlackService()) { this.slackEventBusService = slackEventBusService; this.slackService = slackService; } async handle(context, event) { const slackEvent = await this.verifyAndParseSlackEvent(context, event); const { payload } = slackEvent; switch (payload.type) { case "url_verification": { return { statusCode: 200, body: JSON.stringify({ challenge: payload.challenge }) }; } case "app_rate_limited": case "event_callback": { await this.slackEventBusService.send(slackEvent); return { statusCode: 200, body: JSON.stringify({}) }; } default: return assertSlackEventPayloadUnreachable2(payload); } } async verifyAndParseSlackEvent({ appId }, { body, headers }) { const verified = this.slackService.verifySignature({ appId, body, headers }); if (!verified) { throw new Error( `event verification failed, the event is not from Slack or secrets mis-configured for appId '${appId}'` ); } return this.slackService.parseEvent(body); } }; // src/path-constants.ts var SLACK_PATH_EVENTS_API = "/events"; // src/lambdas/slack-handler.lambda.ts var DEFAULT_HEADERS = { "content-type": "application/json" }; var globalSlackEventBusService = new SlackEventBusService(); var globalSlackService = new SlackService(); var SlackHandlerLambda = class extends BaseLambda { constructor(slackEventHandlerService = new SlackEventHandlerService( globalSlackEventBusService, globalSlackService )) { super(); this.slackEventHandlerService = slackEventHandlerService; } async handle(event) { const appId = event.pathParameters?.appId ?? process.env.SLACK_APP_ID; if (appId === void 0) { throw new Error( "{appId} is undefined, make sure the url includes {appId} path parameter" ); } const result = await this.handleByPath({ appId }, event); return { ...result, headers: { ...DEFAULT_HEADERS, ...result.headers } }; } async handleByPath(handlerContext, event) { const path = event.requestContext.http.path; if (path.endsWith(SLACK_PATH_EVENTS_API)) { return this.slackEventHandlerService.handle(handlerContext, event); } throw new Error(`unknown path: '${path}'`); } }; var handler = createLambdaHandler(new SlackHandlerLambda()); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { SlackHandlerLambda, handler });