Skip to content

fix(auth): enforce session revocation on live sockets, add sign-out to the mobile menu - #113

Merged
pyramation merged 2 commits into
mainfrom
feat/session-revocation-logout
Aug 19, 2026
Merged

fix(auth): enforce session revocation on live sockets, add sign-out to the mobile menu#113
pyramation merged 2 commits into
mainfrom
feat/session-revocation-logout

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

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:

// upgrade: was verifyJwt(token) only
const auth = authenticateSocketToken(token);   // 'invalid-token' | 'session-revoked' | ok
socketSessions.set(ws, auth.sid);              // null for receiver-key sockets

// sweep, plus an immediate call from the revoke/delete-user/logout routes
for (const { ws } of socketsWithRevokedSessions(entries))
  ws.close(4001, 'session-revoked');

The sweep exists because revocation also happens out-of-process — the desktop app writes the same @wavegrid/settings store 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 have sid: null and are never swept; legacy tokens without a sid claim still connect.

Close code 4001 is an application code, so unlike a rejected handshake (which browsers flatten to 1006) the client keeps the meaning: diagnoseConnection short-circuits to sessionExpired without probing, and use-socket stops 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:

POST /api/logout    revokeSession(project, payload.sid)  close its sockets, always { ok: true }

Idempotent on purpose: a client throwing its token away must land on the login screen even if the request fails, so endSessionOnServer swallows errors and the UI clears state regardless. Credential handling moved into lib/auth-storage.ts (token + legacy key cleared, wg_last_user kept 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 with 4001 session-revoked while Bob's stays open and his /api/me still 200s, her token can no longer open a socket (401) or pass /api/me; out-of-process revokeUserSessions closes the socket via the sweep; /api/logout revokes and disconnects. All three fail against main (verified by stashing the src changes).
  • packages/server/__tests__/ws-auth.test.ts, plus new cases in packages/ui/__tests__/connection.test.ts and packages/ui/__tests__/auth-storage.test.ts.
  • pnpm build (includes the desktop tsc --noEmit), pnpm -r run lint, server/ui/settings suites green. pnpm install was needed first — @wavegrid/cli was missing its @wavegrid/osc link 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

@pyramation pyramation self-assigned this Aug 19, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

…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.
@devin-ai-integration
devin-ai-integration Bot force-pushed the feat/session-revocation-logout branch from 66cfedd to b97d1b5 Compare August 19, 2026 01:44
@pyramation
pyramation merged commit 04d8e34 into main Aug 19, 2026
5 checks passed
@devin-ai-integration

Copy link
Copy Markdown
Contributor

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 997b6ae + b97d1b5, started with WG_HEARTBEAT_MS=1000.

Mobile menu Sign out

Phone menu with Sign out

Tapping it lands on the login screen, wg_token is removed, and the session disappears from GET /api/admin/sessionsPOST /api/logout really revokes server-side, so the socket cannot survive the button press.

Revocation while connected — 4001 / "session revoked"

Session ended notice

  • DELETE /api/admin/sessions/:id → phone at the login screen with the session-ended notice in 8 ms.
  • A parallel node ws client on the same session observed exactly code=4001 reason="session revoked" (new hub.ts constants).
  • Revoked token then got 401 on /api/me and 401 on a new WS upgrade; token cleared and TCP connections to the brain stayed flat — no retry with the dead token.
  • Out-of-process revocation (writing the settings store from another process) was caught by the heartbeat sweep in 170 ms at WG_HEARTBEAT_MS=1000.
Blast radius: other user + receiver unaffected

Admin window still driving the show

adminx kept a green status dot and kept painting after the revocation; a receiver-key socket stayed OPEN and received 1560 live command/state frames. Note: the in-process receiver's console sample[0] line stays all-zero even while cannons are painted (pre-existing/unrelated), so receiver liveness was proven via the receiver-key socket instead.

Also sanity-checked the OSC change: wavegrid projects osc beyond with no --host saves BEYOND → 127.0.0.1:8000 instead of erroring.

All assertions passed.

Written by Devin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant