2.0 KiB
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}.