alerting
A five-stage alert-delivery pipeline (dedup, rate-cap-to-digest, IANA-timezone quiet hours, multi-channel send, one audit row) the SOC 2 CC7.2 control.
@caisson/alerting is Caisson's SOC 2 CC7.2 alert-delivery control: a deterministic
five-stage pipeline between an event and a delivered alert. Every dependency, including
the clock, is injected, dedup, rate-cap, quiet hours, delivery, and audit are pure
functions of their inputs.
What it does
- Dedup on an open incident's key:
dedup()suppresses a repeat event while an incident sharing itsdedupeKeyis 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-typeRateCapPolicy; oncemaxPerWindowis reached the outcome flips to"digest"instead of"deliver": noisy alert types back off, they don't vanish. - IANA-timezone quiet hours:
quietHours()resolves the recipient's local hour via the stdlibIntl.DateTimeFormat(no timezone-database dependency) and holds delivery inside the configured window, except a"critical"severity event always delivers. - Four delivery channels behind one port: email, webhook, Slack, and Telegram all
implement the same
AlertChannelport;deliverAll()runs them viaPromise.alland catches every throw into a failedDeliveryResult, so one channel being down never blocks the others. - A structured audit row per outcome:
processAlert()always callsauditSink.record()exactly once: delivered, suppressed, held, or digested.
Install
bun add @caisson/alertingQuickstart
import {
processAlert,
createEmailChannel,
createWebhookChannel,
createInMemoryAuditSink,
} from "@caisson/alerting";
const result = await processAlert(event, {
openIncidents, // { dedupeKey }[] — from your incident store
recentCount, // this recipient's sends in the current rate-cap window
ratePolicy: { maxPerWindow: 5 },
recipientTz: "America/New_York",
quietPolicy: { startHour: 22, endHour: 7 },
now: new Date(),
channels: [createEmailChannel(emailer), createWebhookChannel({ url })],
auditSink: createInMemoryAuditSink(), // or a Postgres-backed AlertAuditSink
});
// result.outcome: "delivered" | "suppressed" | "held" | "digested"The pipeline, in order
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 can short-circuit to its own outcome before a channel is ever touched, and
finish() fires on every path, the audit row is written whether or not anything
actually delivered.
Drivers
| Channel | Factory | Transport |
|---|---|---|
| Capture | createCaptureChannel() | In-memory; test-only. |
createEmailChannel(emailer) | Delegates to an injected @caisson/email Emailer. | |
| Webhook | createWebhookChannel(config) | fetchWithTimeout POST; optional HMAC signature. |
| Slack | createSlackChannel(config) | fetchWithTimeout POST to an incoming-webhook URL. |
| Telegram | createTelegramChannel(config) | fetchWithTimeout POST to a bot-API sendMessage. |
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.
Configuration surface
DEFAULT_EVENT_TYPE_REGISTRY maps eventType -> { defaultSeverity, channels, ratePolicy }, a small seed (four event types), not an exhaustive catalog. Extend it
with your own event types per real usage; AlertEventSchema is the one Zod .strict()
boundary every stage consumes.
Composing with the base
Alerting is a plain package on top of the base substrate: it reuses @caisson/kernel's
fetchWithTimeout and SSRF guards, and delegates its email channel to @caisson/email
rather than opening a second email path. Its audit sink writes to a plain, RLS-forced
Postgres table (alert_audit_log), explicitly not the hash-chained WORM chain that
@caisson/audit-worm owns; the two products stay deliberately distinct.
A real dependency, not a manifest listing
Alerting is a workspace:* dependency the Compliance bundle re-exports at runtime
(export * from '@caisson/alerting'), not a promise on a manifest. Buy it standalone,
or get it composed into Compliance.
retention-runner
The CCPA/GDPR right-to-erasure runner, fans one subject's erasure across every registered store, isolates per-target failure, and writes one reason-tagged audit row.
AI-Production
The production-rigor layer for AI features, token metering with spend caps, a CI eval gate, PII/moderation guardrails, and a versioned prompt registry.