From 1fd284dbe33d05fd5037b172fe652a8a98f7b68d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 17 Sep 2026 10:26:09 +0200 Subject: auth: a sign-out during a renewal must not write half a session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `{ ..._auth }` after the await spreads a null _auth to {}, so the renewal stored a token with no username and no userId. The app renders the signed-in interface from that and throws on user.username[0] — a blank page on every load, in localStorage, until the site's data is cleared. The sign-out wins the race now, and loadAuth treats an identity-less object as signed out so a browser already holding one heals itself. Co-Authored-By: Claude Opus 5 --- .../src/meshbay_hub/static/hub-client.js | 27 +++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js index eead457..81185a5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js @@ -188,11 +188,23 @@ async function _clearKeyDB() { } function loadAuth() { + let auth; try { - return JSON.parse(localStorage.getItem(AUTH_KEY)); + auth = JSON.parse(localStorage.getItem(AUTH_KEY)); } catch { return null; } + // A session is an identity and a token. An object carrying only the token is + // what a sign-out racing a renewal used to write (see `refreshAccessToken`), + // and it is worse than no session: the app renders the signed-in interface + // from it and throws on the first field it reads. That fix stops new ones + // being written; this one lets a browser already holding one heal itself on + // the next load, instead of needing somebody to find the reset button. + if (auth && (!auth.username || !auth.userId)) { + try { localStorage.removeItem(AUTH_KEY); } catch { /* private mode */ } + return null; + } + return auth; } function saveAuth(auth) { @@ -276,6 +288,19 @@ async function refreshAccessToken() { return null; } const data = await r.json(); + // Signing out while this was in flight is not rare — it is the ordinary + // shape of a tab left open: the idle watch signs out at the same moment + // the renewal fires, one second apart in the hub's log. `_auth` is null + // by now, and `{ ..._auth }` spreads null to `{}` without complaining, so + // what got written back was a token and *no identity at all*: an object + // the app believes is a session, renders the signed-in interface from, + // and throws on at the first field it reads — `username[0]`, a blank page + // on every load afterwards, in localStorage, surviving everything but a + // reset of the site's data. + // + // A sign-out that arrives during a renewal wins. There is nothing here + // worth saving over it. + if (!_auth) return null; setAuth({ ..._auth, token: data.access_token, -- cgit v1.2.3