feat(notification): implement the notification delivery platform
Maps the 31-module plan onto the registry's 19 leaves as packages; the two edges the registry forbids (provider->httpclient, inbox->messaging) are replaced by application-owned ports. See docs/notification/module-mapping.md. Acceptance is not delivery: ProviderSubmissionResult refuses to carry a delivery outcome, and AMBIGUOUS is a first-class terminal state that blocks automatic retry and fallback until reconciliation resolves it. Providers: SES (SigV4 + SNS callback), Twilio (X-Twilio-Signature + reconciliation), FCM (FID-primary batch), APNs, Web Push (RFC 8030/8291/8292), SMTP and webhook. Contact points are AES-256-GCM encrypted with a separate HMAC lookup fingerprint; nothing raw reaches a log, metric tag or exception. Dispatch commits the attempt row, calls the provider with no transaction open, then records the outcome; the durable queue uses FOR UPDATE SKIP LOCKED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b5aee50e3
commit
701ba67456
@@ -0,0 +1,65 @@
|
||||
# Provider runbooks
|
||||
|
||||
## SMTP
|
||||
|
||||
| Symptom | Classification | Action |
|
||||
|---|---|---|
|
||||
| Final `2xx` after `DATA` | `CONFIRMED_ACCEPTED` / `PROVIDER_ACCEPTED` | None; this is acceptance, not inbox delivery |
|
||||
| `4yz` | `TRANSIENT_PROVIDER` | Retry under budget and deadline |
|
||||
| `5yz` | `PERMANENT_PROVIDER` or `INVALID_RECIPIENT` | Stop, or invalidate the contact point |
|
||||
| Connection lost after `DATA` | `AMBIGUOUS_SUBMISSION` | Reconcile or escalate; do not resend automatically |
|
||||
|
||||
Connection, read, write and pool-acquire timeouts are all finite. There is no unbounded timeout.
|
||||
|
||||
## Amazon SES
|
||||
|
||||
`MessageId` is acceptance evidence. SES itself documents that it can accept a request and then not
|
||||
send, so `MessageId` is never mapped to `DELIVERED`.
|
||||
|
||||
| Event | Normalized |
|
||||
|---|---|
|
||||
| `Send` | reinforces `PROVIDER_ACCEPTED` |
|
||||
| `Delivery` | `DELIVERY_CONFIRMED` / `NETWORK_OR_CARRIER_ACCEPTED` |
|
||||
| `DeliveryDelay` | delay fact |
|
||||
| `Bounce` (permanent) | `BOUNCED_HARD` plus hard-bounce suppression |
|
||||
| `Bounce` (transient) | `BOUNCED_SOFT`; retry policy input, not a suppression reason |
|
||||
| `Complaint` | complaint fact plus suppression |
|
||||
| `Reject` | `PROVIDER_REJECTED` |
|
||||
| `RenderingFailure` | `TEMPLATE_FAILURE` |
|
||||
|
||||
## Twilio
|
||||
|
||||
`accepted`/`queued` is acceptance only. `sent` is carrier acceptance. `delivered` is device delivery.
|
||||
|
||||
Callbacks are not ordered. A `sent` arriving after `delivered` is stored and ignored by the
|
||||
projection. Missing callbacks are corrected by status polling under the provider rate limit.
|
||||
|
||||
Signature verification uses the canonical external URL from the profile, not the URL the servlet
|
||||
container reconstructed behind a proxy.
|
||||
|
||||
## FCM
|
||||
|
||||
| Error | Classification |
|
||||
|---|---|
|
||||
| `UNREGISTERED` | `INVALID_RECIPIENT`; invalidate the contact point, never retry |
|
||||
| `INVALID_ARGUMENT` | `INVALID_PAYLOAD` |
|
||||
| `QUOTA_EXCEEDED` | `THROTTLED`, exponential backoff |
|
||||
| `UNAVAILABLE` | `TRANSIENT_PROVIDER`, honour `Retry-After`, add jitter |
|
||||
| Credential failure | `AUTHENTICATION`; opens the provider route |
|
||||
|
||||
A batch is one transport call and many attempts. Partial results map back by input index; one
|
||||
transport failure does not become one shared outcome unless the adapter can prove it.
|
||||
|
||||
## APNs
|
||||
|
||||
2xx is acceptance. Environment and topic mismatches are configuration failures, not delivery
|
||||
failures. Sandbox and production tokens are separate namespaces.
|
||||
|
||||
## Web Push
|
||||
|
||||
`TTL` is mandatory by protocol. `201` is acceptance. `404` is an expired subscription per RFC 8030;
|
||||
provider-documented `410` maps the same way. Payloads use `aes128gcm` per RFC 8291 and VAPID JWTs are
|
||||
signed per RFC 8292 with the audience taken from the endpoint origin.
|
||||
|
||||
VAPID key rotation is not ordinary credential rotation: a restricted subscription may need to be
|
||||
re-created, so it is a migration operation.
|
||||
Reference in New Issue
Block a user