import crypto from "node:crypto"; import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; const secrets = new SecretsManagerClient(); const lambda = new LambdaClient(); const EXPENSE_PROCESSOR_FN = process.env.EXPENSE_PROCESSOR_FN; const EXPENSE_SIGNING_SECRET_NAME = process.env.EXPENSE_SIGNING_SECRET_NAME; let cachedSigningSecret; async function getSigningSecret() { if (cachedSigningSecret) return cachedSigningSecret; const { SecretString } = await secrets.send( new GetSecretValueCommand({ SecretId: EXPENSE_SIGNING_SECRET_NAME }) ); cachedSigningSecret = SecretString; return cachedSigningSecret; } function verifySignature(body, timestamp, signature, secret) { if (!timestamp || !signature) return false; const ts = Number(timestamp); if (!Number.isFinite(ts)) return false; if (Math.abs(Date.now() / 1000 - ts) > 300) return false; const base = `v0:${timestamp}:${body}`; const expected = "v0=" + crypto.createHmac("sha256", secret).update(base).digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } export const handler = async (event) => { let body = event.body || ""; if (event.isBase64Encoded) { body = Buffer.from(body, "base64").toString("utf-8"); } const headers = Object.fromEntries( Object.entries(event.headers || {}).map(([k, v]) => [k.toLowerCase(), v]) ); const timestamp = headers["x-slack-request-timestamp"] || ""; const signature = headers["x-slack-signature"] || ""; const secret = await getSigningSecret(); if (!verifySignature(body, timestamp, signature, secret)) { console.log("Signature verification failed"); return { statusCode: 401, body: "unauthorized" }; } const payload = JSON.parse(body); if (payload.type === "url_verification") { return { statusCode: 200, headers: { "Content-Type": "text/plain" }, body: payload.challenge || "", }; } if (payload.type === "event_callback") { await lambda.send( new InvokeCommand({ FunctionName: EXPENSE_PROCESSOR_FN, InvocationType: "Event", Payload: JSON.stringify(payload.event), }) ); } return { statusCode: 200, body: "" }; };