diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-17 10:26:09 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-17 10:26:09 +0200 |
| commit | 1fd284dbe33d05fd5037b172fe652a8a98f7b68d (patch) | |
| tree | 52465b6d438fc7d7bd96f1cf83571069eb0fa3d3 /packages/meshbay-hub/src | |
| parent | a98445ce246509b0d2600df14907f72b448a6d10 (diff) | |
| download | meshbay-1fd284dbe33d05fd5037b172fe652a8a98f7b68d.tar.gz | |
auth: a sign-out during a renewal must not write half a session
`{ ..._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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/hub-client.js | 27 |
1 files changed, 26 insertions, 1 deletions
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, |