Summary
The outbound webhook signing scheme in src/webhooks/signature.rs is inadequate for a payments-adjacent system:
- The signature is a bare HMAC-SHA256 hex of the payload — there is no timestamp binding, so any captured delivery can be replayed indefinitely and receivers have no spec-compliant way to reject stale deliveries.
- There is no scheme versioning (cf. Stripe's
t=<ts>,v1=<sig> format), so the algorithm can never be rotated without breaking every consumer.
- There is no secret rotation story — a leaked webhook secret requires a hard cutover that breaks all receivers simultaneously.
- The doc comment claims the signature proves the request "came from Nova Launch" — a copy-paste artifact from another codebase that must go.
- Delivery retries (
src/webhooks/retry.rs) carry no idempotency/delivery ID, so receivers cannot deduplicate redelivered events.
Task
- Implement a versioned signature header:
X-TipJar-Signature: t=<unix_ts>,v1=<hmac_hex> where the signed message is "{t}.{raw_body}".
- Add a documented tolerance window (default 5 min) and publish receiver-side verification guidance (including constant-time comparison) in
docs/, with reference verification snippets.
- Support dual signing during secret rotation: multiple active secrets produce
v1=<sig1>,v1=<sig2>; add secret versioning to storage and an admin rotation endpoint with audit logging.
- Add a stable
X-TipJar-Delivery-Id (UUID, persisted) and X-TipJar-Event-Type header to every delivery; the same delivery ID must be reused on retries so receivers can deduplicate.
- Retry hardening in
retry.rs: exponential backoff with jitter, per-endpoint circuit breaking after N consecutive failures (reuse services/circuit_breaker.rs), dead-letter persistence of permanently failed deliveries with an admin re-drive endpoint.
- Exhaustive tests: signature format, tolerance boundary (±1s around the window), dual-secret rotation overlap, delivery-ID stability across retries, circuit-breaker transitions.
Acceptance criteria
PR requirements (mandatory)
- ✅ Your PR must pass all checks — PRs with failing or skipped checks will not be merged.
- 📸 You must attach a screenshot in the PR description demonstrating the result (for this issue: a captured webhook request showing the new headers, plus the passing test run).
- 🔗 You must link this issue number in your PR description (e.g.
Closes #<issue-number>). PRs without a linked issue will not be reviewed.
Summary
The outbound webhook signing scheme in
src/webhooks/signature.rsis inadequate for a payments-adjacent system:t=<ts>,v1=<sig>format), so the algorithm can never be rotated without breaking every consumer.src/webhooks/retry.rs) carry no idempotency/delivery ID, so receivers cannot deduplicate redelivered events.Task
X-TipJar-Signature: t=<unix_ts>,v1=<hmac_hex>where the signed message is"{t}.{raw_body}".docs/, with reference verification snippets.v1=<sig1>,v1=<sig2>; add secret versioning to storage and an admin rotation endpoint with audit logging.X-TipJar-Delivery-Id(UUID, persisted) andX-TipJar-Event-Typeheader to every delivery; the same delivery ID must be reused on retries so receivers can deduplicate.retry.rs: exponential backoff with jitter, per-endpoint circuit breaking after N consecutive failures (reuseservices/circuit_breaker.rs), dead-letter persistence of permanently failed deliveries with an admin re-drive endpoint.Acceptance criteria
PR requirements (mandatory)
Closes #<issue-number>). PRs without a linked issue will not be reviewed.