import "server-only"; import { env } from "@/env"; import { logger } from "@/lib/logger"; import { redis } from "@/lib/redis"; import { type SendAlertInput, sendAlert } from "@/lib/services/alert"; // === CrowdSec operational alerts =========================================== // // Thin, fire-and-forget wrapper around the app's alert service for the // reputation pipeline. Every raise is cooldown-gated through a Redis NX lock // (key crowdsec:alert:{key}, TTL = HEALTH_ALERT_COOLDOWN_MIN), so N instances // and flapping conditions surface exactly one alert per window instead of // spamming Discord/email/alert_logs. Falls back to alerting anyway when Redis // is unreachable — a silent quota blowout is worse than one duplicate alert. const ALERT_PREFIX = "crowdsec:alert:"; function cooldownSeconds(): number { const raw = Number(env.HEALTH_ALERT_COOLDOWN_MIN ?? 15); return Math.ceil((Number.isFinite(raw) && raw > 0 ? raw : 15) * 60); } /** * Raise an alert unless the cooldown window is still active. Returns the * sendAlert promise when the alert was actually raised, or false when it was * suppressed. Never throws; the caller may `void` the result on hot paths. */ export async function raiseCrowdsecAlert( key: string, input: { type?: string; severity: SendAlertInput["severity"]; message: string; context?: SendAlertInput["context"]; }, ): Promise>> { if (redis) { try { const acquired = await redis.set( `${ALERT_PREFIX}${key}`, String(Date.now()), "EX", cooldownSeconds(), "NX", ); if (acquired !== "OK") return false; } catch { // Cooldown bookkeeping failed — alert anyway rather than silently drop. } } try { return await sendAlert({ type: "ddos", severity: input.severity, message: input.message, context: input.context, }); } catch (error) { logger.error("[crowdsec-alert] sendAlert raised an unexpected error", { key, err: error, }); return false; } }