Alert pipeline
Five deterministic stages between an event and a delivered alert, dedup, rate-cap, quiet hours, multi-channel send, one audit row.
What it is
The alerting module is Caisson's SOC 2 CC7.2 alert-delivery control: a five-stage pipeline (dedup, rate-cap-to-digest, IANA-timezone quiet hours with a critical override, multi-channel delivery (email, webhook, Slack, Telegram), then a structured audit row) that runs deterministically because every dependency, including the clock, is injected.
What ships in the module
Browser-safe entry point
Import @caisson/alerting/browser inside a client bundle for the event contract, all three decision stages, the delivery port with its isolation wrapper and capture driver, the audit port with its in-memory driver, and processAlert itself. The five network drivers stay on the main entry, which keeps the complete node-capable surface, and every browser-entry export is also on it.
Dedup on an open incident's key
dedup() suppresses a repeat event while an incident sharing its dedupeKey is still open, so a flapping check doesn't re-fire an alert that already has a live incident.
Rate-cap to a digest, never a drop
rateCap() checks the recipient's recent send count against a per-event-type RateCapPolicy; once the window's maxPerWindow is reached the outcome flips to "digest" instead of "deliver", noisy alert types back off, they don't vanish.
IANA-timezone quiet hours, critical overrides
quietHours() resolves the recipient's local hour via Intl.DateTimeFormat (no timezone database dependency) and holds delivery inside the configured window, except a "critical" severity event always delivers, no matter the hour.
Four delivery channels behind one port, isolated
createEmailChannel, createWebhookChannel, createSlackChannel, and createTelegramChannel all implement the same AlertChannel port; deliverAll() runs them via Promise.all and catches every throw into a failed DeliveryResult, so one channel being down never blocks the others.
SSRF-guarded buyer-supplied destinations
Webhook, Slack, and Telegram config URLs pass @caisson/kernel's assertSafePublicUrl at the Zod schema boundary and assertSafePublicUrlResolved again at the fetch call (a DNS-rebinding recheck), and every outbound POST sets redirect: "error" so a 3xx can't hop the request to a private host after the check.
One structured audit row per outcome
processAlert() always calls auditSink.record() exactly once (delivered, suppressed, held, or digested) into a plain, RLS-forced Postgres table (alert_audit_log), explicitly not the hash-chained WORM audit-worm product; the two are kept deliberately distinct.
export async function processAlert(
event: AlertEvent,
deps: ProcessAlertDeps,
): Promise<ProcessAlertResult> {
if (dedup(event, deps.openIncidents)) {
return finish(event, deps, "suppressed", []);
}
if (rateCap(event, deps.recentCount, deps.ratePolicy) === "digest") {
return finish(event, deps, "digested", []);
}
if (
quietHours(event, deps.recipientTz, deps.quietPolicy, deps.now) === "hold"
) {
return finish(event, deps, "held", []);
}
const deliveries = await deliverAll(event, deps.channels);
return finish(event, deps, "delivered", deliveries);
}- Each stage (dedup, rateCap, quietHours) can short-circuit to its own finish() outcome before a channel is ever touched.
- deliverAll only runs after all three gates pass, and finish() fires on every path, the audit row is written whether or not anything actually delivered.