Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
# OpenSpec Change: enterprise-suppression
- **Change ID**: enterprise-suppression
- **Package**: `@suaveplan/suppression`
- **Category**: universal
- **Target path**: `universal/email/suppression`
- **Status**: PROPOSED
- **Date**: 2026-04-19
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/suppression.md`
## Summary
Email suppression list manager — classifies bounces, records unsubscribes, filters outgoing sends, and generates signed one-click unsubscribe links per RFC 8058. Pluggable store.
## Current-state audit (genesis)
Package does not exist yet — fresh build.
## Dependencies
- Workspace: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/types`
- External (via catalog): `zod`, `tslib`, `@opentelemetry/api`
- Dev (via catalog): `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata`
## Preserved public surface (star-exported from main)
- `BounceClassifier`, `classifyBounce`, `BounceType`
- `SuppressionFilter` — filters an outgoing send list against the store.
- `SuppressionManager` — CRUD over suppressions with reason tagging.
- `UnsubscribeLink` — `generateLink`, `verifyLink`, `rotateSecret`.
- `SuppressionStore` interface + `MemorySuppressionStore`.
- Types: `SuppressionReason`, `SuppressionEntry`, `BounceReport`, `FilterResult`, `LinkOptions`.
## New genesis additions
- Codes: `SUPPRESSION.INVALID_ADDRESS`, `SUPPRESSION.STORE_ERROR`, `SUPPRESSION.LINK_EXPIRED`, `SUPPRESSION.LINK_INVALID_SIGNATURE`.
- Zod schemas for every type.
- OTel counter `suppression.filter.total{outcome}`, `suppression.suppressed.total{reason}`.
- Property tests: every classified hard bounce remains suppressed; round-trip of `generateLink → verifyLink`.
## Impact
- Adds one new package: `universal/email/suppression`
- Adds one new spec capability: `suppression`

View File

@@ -0,0 +1,37 @@
# 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}`.

View File

@@ -0,0 +1,38 @@
# Tasks: enterprise-suppression
## 1. Scaffold
- [x] 1.1 `universal/email/suppression/`; 100/100/100/100; guard tests; CLAUDE/README/FOLDERS.
## 2. Errors + schemas
- [x] 2.1 `src/errors/suppression-error.ts` — `SUPPRESSION.*` codes.
- [x] 2.2 Zod schemas.
## 3. Bounce classifier
- [x] 3.1 `src/core/bounce-classifier/bounce-classifier.ts` — RFC 3463 status-code mapper → hard/soft/undetermined.
## 4. Store
- [x] 4.1 `src/stores/suppression.store.ts` — `SuppressionStore` interface + `MemorySuppressionStore`.
## 5. Manager + filter
- [x] 5.1 `src/core/suppression-manager/suppression-manager.ts` — CRUD over suppressions with reasons.
- [x] 5.2 `src/core/suppression-filter/suppression-filter.ts` — `filter(addresses) → FilterResult`.
- [x] 5.3 OTel counters.
## 6. Unsubscribe links
- [x] 6.1 `src/core/unsubscribe-link/unsubscribe-link.ts` — HMAC-SHA256 signed link + TTL; RFC 8058 one-click payload.
## 7. Docs / barrel / subpath exports / README
## 8. Benchmarks
- [x] 8.1 `filter.bench.ts` on 100 k-entry store.
## 9. Ship gate
- [x] 9.1 Verification sequence; fold + archive.