Files
suaveplan/openspec/archive/2026-04-19-enterprise-suppression/specs/suppression/spec.md
2026-05-27 23:29:53 +01:00

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.

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