fix(auth): stop erasing a reset link the signed-in tree never reads (E9.5 review round 4)

Round 4 came back converged — nothing above low, and both findings in one family. This is the half
worth fixing, because the branch introduced it.

Only the signed-out auth card consumes a `#reset=` token, and S2 moved the address-bar cleanup up to
a hook App mounts unconditionally. So opening a password-reset link in a browser that already holds a
session erased the token from the URL while nothing had read it: the page loads, nothing is said, and
clicking the link again does exactly the same, because the token is untouched and the session is
still there. That is the dead end this epic exists to remove, and `main` did not have it — the strip
used to live inside the card, which a signed-in visitor never renders.

An unspent reset fragment now stays, which restores the recovery that shape always had: signing out
reloads with the fragment intact and the form is waiting. Once the card has spent the token a later
reload meets S4's named dead-link screen, which is true. The affirmative version — a banner telling a
signed-in visitor that reset links open in a signed-out window — is logged with DX-15, along with the
`hashchange` half that still only recognises `#verify=`.
This commit is contained in:
2026-09-04 23:14:46 +02:00
parent 3dc14402b0
commit a03bf8d371
3 changed files with 30 additions and 6 deletions
+11 -1
View File
@@ -39,12 +39,22 @@ export function clearEntryUrl(): void {
const params = new URLSearchParams(window.location.search);
const dropped = ONE_TIME_QUERY.filter((k) => params.has(k));
for (const k of dropped) params.delete(k);
// A `#reset=` token that nothing has spent STAYS. Only the signed-out auth card consumes it, so in
// a browser that already holds a session nobody reads it — and erasing it there turned the link
// into a dead end with no explanation: the page loads, nothing happens, and clicking the link
// again does the same, because the token is untouched and the session is still there. Leaving it
// in the address bar keeps the recovery `main` had, where this strip lived inside the auth card:
// sign out (which reloads, fragment intact) and the reset form is waiting. Once the card has used
// it the token is spent, so a later reload meets S4's named dead-link screen — which is true.
const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
const keep = hash.has("reset") ? window.location.hash : "";
if (!dropped.length && !window.location.hash) return;
if (!dropped.length && keep) return;
const qs = params.toString();
window.history.replaceState(
// Preserved, not nulled: it carries scroll restoration and the channel entry.
window.history.state,
"",
`${window.location.pathname}${qs ? `?${qs}` : ""}`,
`${window.location.pathname}${qs ? `?${qs}` : ""}${keep}`,
);
}