fix(signin): key the resend cooldown to the address it was set for (#20) #22

Merged
ben.stull merged 1 commits from fix-20-signin-cooldown-keying into main 2026-06-11 08:22:08 +00:00
Owner

Fixes #20.

The email-step Send button disabled on any running cooldown, so a merchant who mistyped their address was locked out of sending to the corrected one for the rest of the 60s window — a guard the server never had (request_code enforces the cooldown per address, SD-0001 INV-3 → PUC-2c).

Change

  • New frontend/src/cooldown.ts: cooldownAppliesTo(input, cooldownEmail, secondsLeft) — blocks only while the countdown runs AND the input normalizes (strip+lowercase, mirroring normalize_email/INV-2) to the address the cooldown was set for. Unit-tested first (5 cases, incl. the lockout regression).
  • SignIn.tsx tracks cooldownFor (set on successful send and on a 429 resend_cooldown) and gates the email-step button through it. Same address still shows the honest "Resend in Ns"; any other address sends immediately. Code-step resend unchanged.
  • package-lock.json: sync recorded version with package.json (0.4.0).

Verification

  • vitest 8/8, tsc --noEmit clean, backend 68/68 (untouched).
  • Live UI walk (localhost stack): send to wrong address → "Wrong address?" → same address stays disabled with countdown; corrected address shows enabled "Send code" and sends at once → code step for the new address. No console warnings/errors.

Sibling backlog capture: #21 (layered OTP send rate-limit hardening — not in this PR).

Fixes #20. The email-step Send button disabled on **any** running cooldown, so a merchant who mistyped their address was locked out of sending to the corrected one for the rest of the 60s window — a guard the server never had (`request_code` enforces the cooldown **per address**, SD-0001 INV-3 → PUC-2c). **Change** - New `frontend/src/cooldown.ts`: `cooldownAppliesTo(input, cooldownEmail, secondsLeft)` — blocks only while the countdown runs AND the input normalizes (strip+lowercase, mirroring `normalize_email`/INV-2) to the address the cooldown was set for. Unit-tested first (5 cases, incl. the lockout regression). - `SignIn.tsx` tracks `cooldownFor` (set on successful send and on a 429 `resend_cooldown`) and gates the email-step button through it. Same address still shows the honest "Resend in Ns"; any other address sends immediately. Code-step resend unchanged. - `package-lock.json`: sync recorded version with package.json (0.4.0). **Verification** - vitest 8/8, `tsc --noEmit` clean, backend 68/68 (untouched). - Live UI walk (localhost stack): send to wrong address → "Wrong address?" → same address stays disabled with countdown; corrected address shows enabled "Send code" and sends at once → code step for the new address. No console warnings/errors. Sibling backlog capture: #21 (layered OTP send rate-limit hardening — not in this PR).
ben.stull added 1 commit 2026-06-11 08:21:55 +00:00
fix(signin): key the resend cooldown to the address it was set for (#20)
ci / check (push) Has been cancelled
ci / check (pull_request) Has been cancelled
118e925580
The email-step Send button disabled on any running cooldown, so a merchant
who mistyped their address was locked out of sending to the corrected one
for the rest of the 60s window — a guard the server never had: request_code
enforces the cooldown per address (SD-0001 INV-3 -> PUC-2c).

Track which address the running cooldown belongs to (cooldownFor) and gate
the email-step button through cooldownAppliesTo(), which blocks only while
the countdown runs AND the input normalizes (strip+lowercase, mirroring
normalize_email / INV-2) to that address. Same address still shows the
honest 'Resend in Ns'; any other address sends immediately. The code-step
resend is unchanged. Verified in the live UI (wrong address -> Wrong
address? -> corrected address sends at once).

package-lock.json: sync recorded version with package.json (0.4.0).

Closes #20

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ben.stull merged commit 6b405a0f2d into main 2026-06-11 08:22:08 +00:00
Sign in to join this conversation.