fix(auth): enforce session revocation on live sockets, add sign-out to the mobile menu - #113
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…out button Revoking a session only ever mattered on the next request: an open socket kept its JWT and the phone kept driving the show. Sockets are now bound to the session id in their token, checked at handshake and re-checked against the store, and closed with 4001 session-revoked when the row is gone. The UI treats 4001 as 'signed out' and stops reconnecting, and the mobile menu gained a Sign out entry that revokes the session server-side via POST /api/logout.
66cfedd to
b97d1b5
Compare
End-to-end test: session revocation + mobile sign out (rebased onto #114)Re-ran on a real single-machine show (server + built UI + receiver in one process, ring-6) in Chrome at mobile width 390×844, commits Mobile menu Sign out Tapping it lands on the login screen, Revocation while connected — 4001 / "session revoked"
Blast radius: other user + receiver unaffected
Also sanity-checked the OSC change: All assertions passed. |
Summary
Booting a user off did nothing they could see. HTTP already rejected a revoked
sid(/api/me, admin routes), but the WebSocket upgrade only checked the JWT signature — so the phone kept its socket, kept driving lasers, and kept rendering as signed in. Revocation was documented as "takes effect on the next token refresh, never by kicking an open socket"; that is now false, deliberately.Sockets are bound to the session in their token, and a session row that is gone means the socket goes:
The sweep exists because revocation also happens out-of-process — the desktop app writes the same
@wavegrid/settingsstore with no HTTP route to hook — so the session row is the only source of truth.sessionSweepMs()is read per server (WG_SESSION_SWEEP_MS, default 5s) so a test doesn't wait out a show-tuned interval. Receiver-key sockets havesid: nulland are never swept; legacy tokens without asidclaim still connect.Close code
4001is an application code, so unlike a rejected handshake (which browsers flatten to1006) the client keeps the meaning:diagnoseConnectionshort-circuits tosessionExpiredwithout probing, anduse-socketstops scheduling reconnects — previously it retried forever with a credential that could never work again.Signing out now reaches the server, since the token alone stays valid until it expires and would keep its socket alive:
Idempotent on purpose: a client throwing its token away must land on the login screen even if the request fails, so
endSessionOnServerswallows errors and the UI clears state regardless. Credential handling moved intolib/auth-storage.ts(token + legacy key cleared,wg_last_userkept to prefill the form) so it is testable without a browser.The UI change is one entry in the existing phone menu next to the username — no new nav pattern.
Testing
packages/server/__tests__/session-revocation.test.ts(end-to-end over real HTTP +ws): admin revoke closes Alice's socket with4001 session-revokedwhile Bob's stays open and his/api/mestill 200s, her token can no longer open a socket (401) or pass/api/me; out-of-processrevokeUserSessionscloses the socket via the sweep;/api/logoutrevokes and disconnects. All three fail againstmain(verified by stashing thesrcchanges).packages/server/__tests__/ws-auth.test.ts, plus new cases inpackages/ui/__tests__/connection.test.tsandpackages/ui/__tests__/auth-storage.test.ts.pnpm build(includes the desktoptsc --noEmit),pnpm -r run lint, server/ui/settings suites green.pnpm installwas needed first —@wavegrid/cliwas missing its@wavegrid/osclink and failed to build on a clean checkout.Not verified here: nothing was run against Dan's real installation, and the desktop app's own revoke path was only exercised through the shared store rather than the Electron UI.
Coordination
The sibling session's embedded-artist-UI broadcast problem does not share a root cause with this: nothing here touches broadcast fanout, and the only reconnect change is refusing to retry after
4001.Link to Devin session: https://app.devin.ai/sessions/eb1aedd9ddfd4a82a937ac03ebaa5f38
Requested by: @pyramation