codeflash-internal/js/cf-api/endpoints/slack-events.ts
HeshamHM28 a805f4cfbf revert: rollback PR #2601 and dependent fixes to ec39cd51
Reverts the following commits from main:
- d7a8b8f2 perf: fix CI build + lazy-load heavy libs + parallelize DB queries (#2601)
- 48b5e2b4 fix: make tree-sitter WASM build failure non-fatal when cache exists (#2602)
- c372b6bc Merge pull request #2603 from codeflash-ai/fix/deploy-build-common
- b656bb1d fix: cf-api deploy broken by pnpm workspace migration
- c1b0076c fix: align TypeScript versions to deduplicate @prisma/client in pnpm
- 09ed4d4b fix: use redirect instead of throw for auth failures during prerender
- 71127055 fix: redirect remaining auth throws that crash prerendering

PR #2601 introduced 18 bugs including 5 authorization bypass vulnerabilities:
- Cross-org data access via forged currentOrganizationId cookie
- Cross-repo/cross-org member role escalation and deletion (unscoped lookups)
- Missing replayTests/concolicTests in approval flow
- repository_id filter silently broken for personal accounts
- Tests mocking wrong Prisma method ($queryRawUnsafe vs $queryRaw)

The subsequent PRs (#2602, #2603, and follow-up commits) were dependent
fixes for issues caused by #2601 and are reverted together.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:59:11 +00:00

151 lines
4.6 KiB
TypeScript

import { Request, Response } from "express"
import * as crypto from "crypto"
import { posthog } from "../analytics.js"
import { processReaction } from "../github/optimization_approval.js"
import * as Sentry from "@sentry/node"
import { logger } from "../utils/logger.js"
import { forbidden, internalServerError } from "../exceptions/index.js"
// Dependencies interface for easier testing
export interface HandleSlackEventsDependencies {
crypto: typeof crypto
posthog: typeof posthog
processReaction: typeof processReaction
Sentry: typeof Sentry
getSlackSigningSecret: () => string | undefined
getCurrentTime: () => number
console: typeof console
}
// Default dependencies
let dependencies: HandleSlackEventsDependencies = {
crypto,
posthog,
processReaction,
Sentry,
getSlackSigningSecret: () => process.env.SLACK_SIGNING_SECRET,
getCurrentTime: () => Math.floor(Date.now() / 1000),
console,
}
// For testing - allow dependency injection
export function setHandleSlackEventsDependencies(deps: Partial<HandleSlackEventsDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetHandleSlackEventsDependencies() {
dependencies = {
crypto,
posthog,
processReaction,
Sentry,
getSlackSigningSecret: () => process.env.SLACK_SIGNING_SECRET,
getCurrentTime: () => Math.floor(Date.now() / 1000),
console,
}
}
/**
* Verify that the request is coming from Slack
*/
export function verifySlackRequest(req: Request): boolean {
const SLACK_SIGNING_SECRET = dependencies.getSlackSigningSecret()
if (!SLACK_SIGNING_SECRET) {
logger.warn("SLACK_SIGNING_SECRET not configured. Skipping request verification.", req)
return true
}
const slackSignature = req.headers["x-slack-signature"] as string
const slackTimestamp = req.headers["x-slack-request-timestamp"] as string
if (!slackSignature || !slackTimestamp) {
return false
}
// Prevent replay attacks - reject requests older than 5 minutes
const currentTime = dependencies.getCurrentTime()
if (Math.abs(currentTime - parseInt(slackTimestamp)) > 300) {
return false
}
const requestBody = typeof req.body === "string" ? req.body : JSON.stringify(req.body)
const baseString = `v0:${slackTimestamp}:${requestBody}`
const hmac = dependencies.crypto.createHmac("sha256", SLACK_SIGNING_SECRET)
const signature = "v0=" + hmac.update(baseString).digest("hex")
return dependencies.crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(slackSignature))
}
/**
* Handle Slack events for the approval system
*/
export async function handleSlackEvents(req: Request, res: Response) {
try {
// Verify the request is from Slack
if (!verifySlackRequest(req)) {
logger.error("Failed to verify Slack request", req)
throw forbidden("Invalid Slack request signature")
}
// Handle URL verification (required when setting up events)
if (req.body.type === "url_verification") {
logger.info("Handling Slack URL verification challenge", req)
return res.json({ challenge: req.body.challenge })
}
// Acknowledge receipt quickly
res.status(200).send("OK")
const event = req.body.event
if (event) {
logger.info(`Processing Slack event: ${event.type}`, req, {
eventType: event.type,
})
if (event.type === "reaction_added") {
const processed = await dependencies.processReaction(event)
if (processed) {
dependencies.posthog?.capture({
distinctId: "system",
event: "slack-approval-reaction-processed",
properties: {
reaction: event.reaction,
result: "success",
},
})
}
}
}
} catch (error) {
// Re-throw AppExceptions to be handled by GlobalExceptionHandler if headers not sent
if (error && typeof error === "object" && "getHttpStatus" in error) {
if (!res.headersSent) {
throw error
}
// If headers already sent, just log it
logger.errorWithSentry(`Error handling Slack event: ${error}`, req, {}, error as Error)
dependencies.Sentry.captureException(error)
return
}
logger.errorWithSentry(`Error handling Slack event: ${error}`, req, {}, error as Error)
// If we haven't sent a response yet, throw an error
if (!res.headersSent) {
throw internalServerError("Error processing Slack event")
}
dependencies.Sentry.captureException(error)
// Log to monitoring
dependencies.posthog?.capture({
distinctId: "system",
event: "slack-optimization-approval-error",
properties: {
error: String(error),
},
})
}
}