|
| 1 | +# Provider-neutral delivery model |
| 2 | + |
| 3 | +HayaSend separates provider-neutral delivery truth from the existing |
| 4 | +Resend-compatible message response. The first version is a contract only: it |
| 5 | +does not change the public API, current stores, or current AWS runtime behavior. |
| 6 | +Subsequent Gate 1 work will persist and operate on these records. |
| 7 | + |
| 8 | +The source contract is |
| 9 | +[`src/core/delivery-model.ts`](../src/core/delivery-model.ts), and |
| 10 | +`npm run check:conformance` prevents the published |
| 11 | +[`delivery-record.v1.schema.json`](../schemas/delivery-record.v1.schema.json) |
| 12 | +from drifting from it. |
| 13 | + |
| 14 | +## Records and identities |
| 15 | + |
| 16 | +| Record | Identity | Mutable fields | |
| 17 | +| --- | --- | --- | |
| 18 | +| Message | Existing opaque `email_…` ID | Derived aggregate and timestamps | |
| 19 | +| Recipient | Random opaque `rcpt_…` ID | Lifecycle, latest attempt, timestamp | |
| 20 | +| Attempt | Random opaque `attempt_…` ID | Submission result and completion | |
| 21 | +| Provider event | Provider plus provider event ID, or a normalized-event digest | None after append | |
| 22 | +| Outbox item | Message, job type, and generation | Lease, attempts, dispatch result | |
| 23 | + |
| 24 | +Recipient and attempt IDs require at least 128 bits of URL-safe opaque input. |
| 25 | +The adapter or service that creates them must use a cryptographically secure |
| 26 | +generator. It must never derive either ID from an address. Envelope roles are |
| 27 | +normalized to `to`, `cc`, or `bcc`, while the address remains a customer data |
| 28 | +plane field. |
| 29 | + |
| 30 | +`createOutboxIdentity` is deterministic so a reconciler can publish the same |
| 31 | +job after a crash without inventing a second logical operation. Generation |
| 32 | +starts at zero and changes only for a deliberately distinct operation. |
| 33 | + |
| 34 | +`createProviderEventIdentity` prefers a privacy-safe opaque provider event ID. |
| 35 | +If the provider does not supply one, its adapter must first remove |
| 36 | +unrecognized and private fields, canonically encode the normalized event, and |
| 37 | +supply the SHA-256 digest. The domain package deliberately does not import a |
| 38 | +crypto runtime or accept raw provider payloads. |
| 39 | + |
| 40 | +## Lifecycle meanings |
| 41 | + |
| 42 | +- A message aggregate is derived from recipient records; it cannot replace |
| 43 | + recipient truth. |
| 44 | +- An attempt covers one provider submission and one or more recipient IDs. |
| 45 | + `ambiguous` means the provider might have accepted the submission but the |
| 46 | + local result could not be committed. |
| 47 | +- Provider events are immutable. `terminal` records the adapter's normalized |
| 48 | + interpretation, while later transition logic must keep complaint and |
| 49 | + suppression outcomes safety-sticky. |
| 50 | +- An outbox row is pending when it has no dispatch timestamp. A lease owner and |
| 51 | + expiry appear together. A dispatched row cannot retain a lease. |
| 52 | + |
| 53 | +Timestamps are offset-aware ISO 8601 values. Record schema version, provider |
| 54 | +name, provider adapter version, and capability document version are explicit |
| 55 | +so migrations and conformance evidence can reject silent drift. |
| 56 | + |
| 57 | +## Privacy boundary |
| 58 | + |
| 59 | +Addresses are allowed only on recipient records in the customer data plane. |
| 60 | +Message intent is represented by a digest, diagnostics use a fixed allowlist, |
| 61 | +and records have no fields for subjects, bodies, attachments, raw SMTP |
| 62 | +responses, stack traces, credentials, or signed URLs. Addresses and content |
| 63 | +must never be metric dimensions, public identifiers, or default operator |
| 64 | +output. |
0 commit comments