Skip to content

Commit ae8875c

Browse files
authored
feat: define provider-neutral delivery records (#107)
## Outcome Adds the provider-neutral Gate 1 domain contract from #96 without changing the public API, current stores, or AWS runtime behavior. - versioned message, recipient, attempt, provider-event, and outbox records - opaque recipient and attempt IDs; normalized To/Cc/Bcc roles - deterministic outbox and provider-event identities with no address-derived keys - fixed privacy-safe diagnostic categories and explicit provider/capability versions - generated JSON Schema with reproducible generation and CI drift checks - lifecycle, ambiguity, immutability, and privacy documentation Closes #96 Parent direction: #81 Execution plan: #84 ## Local evidence - `npm run check` - `npm run check:conformance` - `npm test` (24 files, 180 tests) - `npm run build` - `npm run lint:openapi` - `sam validate --lint --template-file template.yaml --region us-east-1` - `actionlint` - `npm audit --audit-level=low` (0 vulnerabilities) - `npm outdated --json` (`{}`) - `npm pack --dry-run --json` - `gitleaks git --staged` (no leaks) - `git diff --check` Signed-off-by: Yusuke Hayashi <yusuke8h@gmail.com>
1 parent a80e870 commit ae8875c

10 files changed

Lines changed: 1381 additions & 2 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ minor releases before v1.0.
1212
replay finds a stored queued or scheduled email.
1313
- Publish a versioned AWS SES capability document, provider-neutral
1414
conformance cases, and generated schemas that CI checks for drift.
15+
- Define versioned provider-neutral message, recipient, attempt, provider-event,
16+
and outbox records with privacy-safe diagnostics and deterministic identities.
1517

1618
## 0.1.0 - 2026-07-26
1719

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ implemented provider today. HayaSend never logs message bodies.
1313
[Project site](https://haya-inc.github.io/hayasend/) ·
1414
[Compatibility](docs/compatibility.md) ·
1515
[Provider capabilities](docs/provider-capabilities.md) ·
16+
[Delivery model](docs/delivery-model.md) ·
1617
[Execution plan](docs/execution-plan.md) ·
1718
[Support](SUPPORT.md)
1819

docs/delivery-model.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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.

docs/provider-capabilities.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ The generated artifacts are:
1515
— capability document schema;
1616
- [`schemas/conformance-result.v1.schema.json`](../schemas/conformance-result.v1.schema.json)
1717
— evidence report schema.
18+
- [`schemas/delivery-record.v1.schema.json`](../schemas/delivery-record.v1.schema.json)
19+
— provider-neutral message, recipient, attempt, event, and outbox records.
1820

1921
`npm run check:conformance` regenerates every artifact in memory and fails if a
2022
committed file is absent or stale. This prevents source types, published JSON,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"cost:estimate": "node scripts/aws-cost-model.mjs",
3131
"dev": "tsx src/server.ts",
3232
"doctor": "tsx src/cli.ts doctor",
33+
"generate:conformance": "tsx scripts/check-conformance.ts --write",
3334
"lint:openapi": "redocly lint openapi.yaml",
3435
"start": "node dist/server.js",
3536
"test": "vitest run",

0 commit comments

Comments
 (0)