fix(e2e): exempt non-prod registration cap + force JMAP public origin (BUNYIP-150) #225

Merged
nrupard merged 1 commit from fix/bunyip-150-register-nonprod-jmap-origin into main 2026-06-25 16:41:30 +02:00
Owner

What

Finish BUNYIP-150 by fixing the two root causes that kept the email-driven e2e specs (magic-link, password-reset, change-email) red. Consolidates four fragmented attempts into one change.

Root causes + fixes

  1. Registration rate limit (BUNYIP-196/197). /v1/auth/register is capped 3/hour/IP - a production anti-abuse control. The deployed-instance e2e suite self-provisions disposable accounts (each needs a fresh unique email, so they cannot be pre-seeded/reused like the shared login account) from the single CI runner egress IP, so registrations accumulate across serial runs in the one-hour window and trip a spurious 429. Fix (bunyip-api/src/handlers/auth.rs::register): apply the cap in production ONLY (if config.is_production()); staging/dev register unthrottled, with the auto-ban still catching abusive bursts.
  2. JMAP apiUrl origin. Stalwart advertises its session apiUrl as the internal http://mail.a8n.run:8080/jmap/, unreachable from the CI runner. lib/mail-sink.ts:jmapSession now keeps only the apiUrl path and forces the public origin (the .well-known/jmap GET already proved 443 reachability).

Main's three separate specs are kept as-is - once the cap no longer throttles staging they each register one disposable account and pass, so the combine-into-one-spec workaround is unnecessary (YAGNI).

Supersedes / closes

  • #221 (combine specs + JMAP fix): salvages the JMAP fix; the combine was only a cap workaround.
  • #222 (configurable REGISTRATION_RATE_LIMIT_MAX): superseded - non-prod exemption needs no env knob. The docker env it relied on (c-01 REGISTRATION_RATE_LIMIT_MAX=100) is reverted in a companion docker PR.
  • #223 (BUNYIP-196, skip-on-429): a symptom mask (greens the suite by not testing) and it lacked the JMAP fix. Dropped per root-cause discipline.
  • #224 (BUNYIP-197, non-prod exempt): this PR adopts its approach.

Deploy ordering (important)

The registration fix tests the DEPLOYED instance, so it only takes effect after this image is deployed to staging. The PR's own pre-merge e2e run can still 429 against the not-yet-redeployed staging; it goes green on the post-merge run. No 429-skip is added to mask that. After merge: staging rebuilds :latest -> redeploy c-01 bunyip-api -> the next e2e run is green.

Verification

just check-container green (fmt + clippy + lib tests); tsc --noEmit clean. Full green requires the post-merge staging redeploy.

🤖 Generated with Claude Code

## What Finish BUNYIP-150 by fixing the two root causes that kept the email-driven e2e specs (magic-link, password-reset, change-email) red. Consolidates four fragmented attempts into one change. ## Root causes + fixes 1. **Registration rate limit (BUNYIP-196/197).** `/v1/auth/register` is capped 3/hour/IP - a production anti-abuse control. The deployed-instance e2e suite self-provisions disposable accounts (each needs a fresh unique email, so they cannot be pre-seeded/reused like the shared login account) from the single CI runner egress IP, so registrations accumulate across serial runs in the one-hour window and trip a spurious 429. Fix (`bunyip-api/src/handlers/auth.rs::register`): apply the cap in production ONLY (`if config.is_production()`); staging/dev register unthrottled, with the auto-ban still catching abusive bursts. 2. **JMAP apiUrl origin.** Stalwart advertises its session `apiUrl` as the internal `http://mail.a8n.run:8080/jmap/`, unreachable from the CI runner. `lib/mail-sink.ts:jmapSession` now keeps only the apiUrl path and forces the public origin (the `.well-known/jmap` GET already proved 443 reachability). Main's three separate specs are kept as-is - once the cap no longer throttles staging they each register one disposable account and pass, so the combine-into-one-spec workaround is unnecessary (YAGNI). ## Supersedes / closes - **#221** (combine specs + JMAP fix): salvages the JMAP fix; the combine was only a cap workaround. - **#222** (configurable `REGISTRATION_RATE_LIMIT_MAX`): superseded - non-prod exemption needs no env knob. The docker env it relied on (c-01 `REGISTRATION_RATE_LIMIT_MAX=100`) is reverted in a companion docker PR. - **#223** (BUNYIP-196, skip-on-429): a symptom mask (greens the suite by not testing) and it lacked the JMAP fix. Dropped per root-cause discipline. - **#224** (BUNYIP-197, non-prod exempt): this PR adopts its approach. ## Deploy ordering (important) The registration fix tests the DEPLOYED instance, so it only takes effect after this image is deployed to staging. The PR's own pre-merge e2e run can still 429 against the not-yet-redeployed staging; it goes green on the post-merge run. No 429-skip is added to mask that. After merge: staging rebuilds `:latest` -> redeploy c-01 bunyip-api -> the next e2e run is green. ## Verification `just check-container` green (fmt + clippy + lib tests); `tsc --noEmit` clean. Full green requires the post-merge staging redeploy. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(e2e): exempt non-prod registration cap + force JMAP public origin (BUNYIP-150)
Some checks failed
E2E / Playwright against deployment (pull_request) Failing after 53s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m16s
Create release / Create release from merged PR (pull_request) Has been skipped
5cb62c81cb
Finish BUNYIP-150 by fixing the two root causes that kept the email-driven e2e specs (magic-link, password-reset, change-email) red, consolidating four fragmented attempts (PRs #221/#222/#223/#224, issues BUNYIP-196/197) into one change.

1. Registration rate limit. `/v1/auth/register` is capped 3/hour/IP as a production anti-abuse control. The deployed-instance e2e suite self-provisions disposable accounts (each needs a fresh unique email, so they cannot be pre-seeded or reused like the shared login account) from the single CI runner egress IP, so registrations accumulate across serial runs in the one-hour window and trip a spurious 429. The cap is a production concern, so apply it in production only (`if config.is_production()`); staging/dev register unthrottled, with the auto-ban still catching abusive bursts. This drops the need for the configurable-limit env knob (superseded) and for any per-run 429 tolerance.

2. JMAP apiUrl origin. Stalwart advertises its session apiUrl as the internal http://mail.a8n.run:8080/jmap/, which the CI runner cannot reach. lib/mail-sink.ts:jmapSession now keeps only the apiUrl path and forces the origin back to the public sink base (the .well-known/jmap GET already proved 443 reachability).

Main's three separate specs are kept as-is: once the cap no longer throttles staging, they register one disposable account each and pass, so the combine-into-one-spec workaround is unnecessary (YAGNI).

Deploy ordering: the registration fix tests the DEPLOYED instance, so it only takes effect after this image is deployed to staging. A pre-merge e2e run can still 429 against the not-yet-redeployed staging; it goes green on the post-merge run. Per root-cause discipline no 429-skip is added to mask that.

just check-container green; tsc --noEmit clean.

#BUNYIP-150
#BUNYIP-196
#BUNYIP-197

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch fix/bunyip-150-register-nonprod-jmap-origin 2026-06-25 16:41:31 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!225
No description provided.