CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

fix(spine): receive-chain breaks + send-path traps from the 2026-08-07 spine audit (#164, #165) #4037

Merged⚡ AI-generatedXSccantynz wants to mergefix/spine-audit-2026-08-07mainopened 25d ago
ccantynzcommented 25d ago

Originally written by @ccantynz-alt on GitHub.
Imported from https://github.com/ccantynz-alt/AlecRae.com/pull/94


Full verification pass over the mail spine ("run checks through our spine"): forced-uncached test suite (48/48 tasks green), typecheck, lint, plus three structural sweeps — stub/fabrication hunt, built-but-unwired importer mapping, and an end-to-end producer↔consumer contract trace of both mail chains.

Send chain verified structurally sound (queue names, job shapes, DKIM/suppression/warm-up/VERP all matching). Receive chain had six real breaks — in services/inbound, the service about to be deployed for business email — all fixed here, plus the send-path traps the trace surfaced.

Receive (services/inbound) — 47 new tests

  • defer verdict → 451, stores nothing (was: filter failure delivered unfiltered mail to the inbox with 250 OK)
  • reject → generic 550 5.7.1 (was 451-retry-forever, leaking the spam score as a tuning oracle)
  • same-account multi-recipient delivery merges via onConflictDoNothing on the dedup index (was: crash → 451 → endless redelivery)
  • STARTTLS honestly absent: 502, never marks the session secure (was advertised+acked with no handshake, defeating requireTls) — real TLS tracked as #168
  • VERP bounce domains (bounce.<hosted> only, no generic subdomain inheritance) accepted so async DSNs finally reach suppression; non-DSN mail to a bounce domain dropped, never bounced
  • DSN processing added to both HTTP ingest paths; forward rules deliver locally with a warning instead of silently discarding; HTTP ingest fails closed (503) without INBOUND_WEBHOOK_SECRET; source: "inbound"

Send (services/mta + apps/api) — 30 new tests

  • per-ISP daily counters reset on the UTC date boundary (were increment-only → progressive self-throttle to zero)
  • warm-up start date persisted in Redis (SET NX) — restarts no longer reset the ramp to day 1
  • signing-domain lookup scoped by accountId; retry policy in defaultJobOptions (agent mail had 1 attempt vs /v1/messages' 8)
  • UPSTASH_REDIS_URL fallback removed everywhere + structural test (REDIS_URL is the single source — the #149 split-brain class); health resolves Redis/queue-name through lib/queue.ts
  • daily DNS liveness job registered at startup (had zero callers while the send path's stale-DNS gate assumed it ran); MTA queue-depth/listener health checks registered; start script; source: "outbound"; fly.toml marked not-production

Docs

DKIM_PRIVATE_KEY/DKIM_SELECTOR env vars removed from templates/runbooks — no code reads them (keys live per-domain, encrypted, in the domains table); template MTA_HOSTNAME corrected mx1smtp. One genuinely flaky test fixed (dynamic import inside a test body → timeout under load → state bleed into the next test).

CLAUDE.md gains open issues #166–#170 (fabricated-output batch, dead-module adopt-or-delete, real TLS, inbound email.received events, residuals); the full fix record is appended to the 2026-08-07 archive.

Gates: tests 48/48 tasks (inbound 67, MTA 329, api 462), typecheck 36/36, lint 0 errors, docs check green.

🤖 Generated with Claude Code

Cross-repo impact

See what breaks downstream if this PR merges.

Analyze →
⮌ Merged

This pull request was merged into main.

c comment · e edit title · m merge · a approve · r request changes · ? shortcuts