@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
JavaScript
;
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
});