Skip to content

feat: sign out that actually ends the session - #297

Merged
anscg merged 1 commit into
mainfrom
feat/sign-out
Oct 8, 2026
Merged

anscg merged 1 commit into
mainfrom
feat/sign-out

Conversation

@anscg

@anscg anscg commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

user.signOut only deleted a lapse-auth cookie the client doesn't use (auth is a bearer token in localStorage), and nothing in the UI called it. There was no way to sign out, and even calling the endpoint left the JWT valid for up to 30 days.

Changes

Server

  • revokeAccessToken adds the token's SHA-256 to a Redis denylist with a TTL matching the JWT's remaining lifetime. Hashing it means a Redis dump doesn't expose live credentials, and the TTL keeps the denylist bounded.
  • The revocation check runs in both getAuthenticatedUser and the OAuth model's getAccessToken.
  • user.signOut revokes the calling token and returns an error if Redis fails, so the client never believes it's signed out when it isn't. It still clears the legacy cookie.

Client

  • signOut clears account-scoped state: the token, lapse:cache.*, Lookout session tokens, and lapse:* sessionStorage. It deliberately keeps the OPFS device store, which holds legacy encryption keys and unrecovered recordings.
  • It then does a full location.replace("/") so no in-memory state survives.
  • Other open tabs sign out too, via the storage event.
  • New SignOutModal, opened from a "Sign out" entry in Settings:
    • It confirms the scope ("this device only") and that timelapses and drafts stay safe in the account.
    • It warns if this browser has an unpublished Lookout recording.
    • If the server can't be reached, it offers "Sign out anyway" (local only).
  • The mobile "You" button now opens Settings, so mobile users can sign out too.

Verification

  • tsc --noEmit passes for the server.
  • The client has no new type errors. The existing errors come from the unbuilt vendored @lookout/react and generated build info.
  • ESLint's own config crashes on load in this repo, so linting was not run.
  • Not tested in a browser.

🤖 Generated with Claude Code

`user.signOut` only cleared a `lapse-auth` cookie we no longer use, and
nothing in the UI called it - the bearer token in localStorage stayed
valid for up to 30 days.

- Server: revoke the caller's access token via a Redis denylist keyed by
  the token's SHA-256, expiring when the JWT would. Checked in both
  `getAuthenticatedUser` and the OAuth model.
- Client: forget account-scoped local state (token, caches, Lookout
  session tokens, auth sessionStorage) while keeping device-level OPFS
  data, then do a full navigation so no in-memory state survives.
  Other open tabs follow along via the `storage` event.
- UI: "Sign out" in Settings with a confirmation dialog that reassures
  about unpublished recordings and offers a local-only fallback if the
  server can't be reached. The mobile "You" button now opens Settings,
  so sign out is reachable there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anscg
anscg merged commit 88c7ea4 into main Oct 8, 2026
7 checks passed
@anscg
anscg deleted the feat/sign-out branch October 8, 2026 21:20
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