Files
2026-05-27 23:29:53 +01:00

38 lines
2.0 KiB
Markdown

# Capability Spec: suppression
**Package**: `@suaveplan/suppression`
**Version**: 0.0.0 (genesis)
**Status**: ADDED
---
## ADDED Requirements
### SUP-01 — Bounce classification
`classifyBounce(report)` MUST map SMTP enhanced status codes (RFC 3463) to `BounceType` (`'hard' | 'soft' | 'technical' | 'policy' | 'unknown'`). Hard and policy bounces MUST add permanent suppressions; soft bounces MUST increment a soft-bounce counter and auto-suppress after the configured threshold.
### SUP-02 — Store contract
`SuppressionStore` MUST provide `add`, `remove`, `has`, `get`, `list`, `purgeExpired`. `MemorySuppressionStore` MUST implement all methods synchronously. Alternative stores (DB, Redis) adhere to the same contract via async variants.
### SUP-03 — Manager API
`SuppressionManager` MUST normalise email addresses (lowercase, trim) before storage. Invalid addresses MUST surface as `SUPPRESSION.INVALID_ADDRESS`. Entries MUST carry `{ address, reason: SuppressionReason, bounceType?, createdAt, expiresAt?, source? }`.
### SUP-04 — Filter contract
`SuppressionFilter.filter(addresses)` MUST return `FilterResult { allowed, suppressed }`. Addresses MUST be looked up in a single batched pass.
### SUP-05 — Unsubscribe links
`UnsubscribeLink.generateLink({ address, list, ttlSeconds })` MUST produce a URL carrying an HMAC-SHA256 signature over `address|list|expiry`. `verifyLink(url, now?)` MUST reject expired or tampered links with `SUPPRESSION.LINK_EXPIRED` / `SUPPRESSION.LINK_INVALID_SIGNATURE`. `rotateSecret(newSecret, gracePeriod?)` MUST accept both old and new signatures during the grace period.
### SUP-06 — RFC 8058 one-click payload
The link MUST include the List-Unsubscribe-Post header form (`List-Unsubscribe=One-Click`) and accept a POST that honours the unsubscribe without further user interaction.
### SUP-07 — Observability
Every filter call MUST record `suppression.filter.total{outcome}`. Every suppression MUST record `suppression.suppressed.total{reason}`.