feat(desktop): keep the login tokens only in the OS keychain (#105 step 2) #445

Merged
jared merged 1 commits from feat/keychain-session into lotus 2026-10-23 20:59:25 -04:00
Owner

Fixes #105 (step 2). Owner decision 2026-10-23: do it, with the fallback.

On the desktop app with a working keychain (Windows Credential Manager), the login tokens leave localStorage. Web, Linux and older desktop builds are unchanged.

Design

  • localStorage keeps a token-less shell (cinny_session_v1 with tokensIn: 'keychain': user, device, server, expiry, OIDC metadata) and no legacy token keys.
  • The full session lives in memory, read from the keychain at boot, before anything reads the session. So getFallbackSession() and its ~20 synchronous callers are untouched.
  • Never fewer than one durable copy: a new session (login, OIDC token refresh) is written to localStorage in full first. The mirror (step 1) writes the keychain, reads it back, and only then strips the tokens. A refresh invalidates the old refresh token, so there's never a moment when only memory holds the newest one.
  • Migration: an existing install has the full session in localStorage, mirrored since step 1. It boots as before and is stripped once the keychain copy is verified. If the keychain write fails, nothing is stripped.
  • Fallback, no silent logout: if the shell is there but the keychain entry is missing or unreadable, the app shows "Couldn't read your sign-in" with Retry and Sign in again. The shell is kept, and the mirror isn't started (it would have cleared the entry it couldn't read).
  • The boot step can't block mounting: any unexpected error means "no keychain", i.e. the old behaviour.

Verified

  • Real Windows (cinny-desktop smoke run 2820, a build of this branch, Credential Manager): a pre-step-2 session in localStorage moved to the keychain on boot (tokensIn: keychain, no token in the blob or legacy keys, the token in Credential Manager), and after a restart the app was still signed in. All 13 smoke checks passed. The check is now part of the Windows smoke test (cinny-desktop PR).
  • Unit tests (8 new, against a fake keychain): web unchanged; migration then the next boot; refresh written in full then stripped; a failing keychain write keeps the tokens; a missing entry → missing; an unreadable keychain → error; another device's tokens never pair; logout clears memory, disk and the keychain. The full suite (1,418) passes, and lint is at 29.

Rollback note: a desktop build older than this would find no tokens in localStorage after migrating, i.e. it would show the login screen. Desktop only auto-updates forward, so this only matters for a manual downgrade.

🤖 Generated with Claude Code

https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5

Fixes #105 (step 2). Owner decision 2026-10-23: do it, with the fallback. On the **desktop app with a working keychain** (Windows Credential Manager), the login tokens leave localStorage. Web, Linux and older desktop builds are unchanged. **Design** - localStorage keeps a **token-less shell** (`cinny_session_v1` with `tokensIn: 'keychain'`: user, device, server, expiry, OIDC metadata) and **no legacy token keys**. - The full session lives **in memory**, read from the keychain at boot, before anything reads the session. So `getFallbackSession()` and its ~20 synchronous callers are untouched. - **Never fewer than one durable copy:** a new session (login, OIDC token refresh) is written to localStorage **in full first**. The mirror (step 1) writes the keychain, reads it back, and **only then** strips the tokens. A refresh invalidates the old refresh token, so there's never a moment when only memory holds the newest one. - **Migration:** an existing install has the full session in localStorage, mirrored since step 1. It boots as before and is stripped once the keychain copy is verified. If the keychain write fails, nothing is stripped. - **Fallback, no silent logout:** if the shell is there but the keychain entry is missing or unreadable, the app shows **"Couldn't read your sign-in"** with **Retry** and **Sign in again**. The shell is kept, and **the mirror isn't started** (it would have cleared the entry it couldn't read). - The boot step can't block mounting: any unexpected error means "no keychain", i.e. the old behaviour. **Verified** - **Real Windows** (cinny-desktop smoke run 2820, a build of this branch, Credential Manager): a pre-step-2 session in localStorage moved to the keychain on boot (`tokensIn: keychain`, no token in the blob or legacy keys, the token in Credential Manager), and **after a restart the app was still signed in**. All 13 smoke checks passed. The check is now part of the Windows smoke test (cinny-desktop PR). - **Unit tests** (8 new, against a fake keychain): web unchanged; migration then the next boot; refresh written in full then stripped; a failing keychain write keeps the tokens; a missing entry → `missing`; an unreadable keychain → `error`; another device's tokens never pair; logout clears memory, disk and the keychain. The full suite (1,418) passes, and lint is at 29. **Rollback note:** a desktop build older than this would find no tokens in localStorage after migrating, i.e. it would show the login screen. Desktop only auto-updates forward, so this only matters for a manual downgrade. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared added 1 commit 2026-10-23 20:34:14 -04:00
feat(desktop): keep the login tokens only in the OS keychain (#105 step 2)
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m50s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 20m30s
5f3c5d370a
On the desktop app with a working keychain (Windows Credential Manager),
the tokens leave localStorage: it keeps a token-less shell blob
(tokensIn: keychain) and the full session lives in memory, read from the
keychain at boot before anything reads the session, so getFallbackSession
stays synchronous. Web, Linux and older desktop builds are unchanged.

- A new session (login, token refresh) is written to localStorage in full
  first; the mirror verifies the keychain copy and only then strips the
  tokens, so one durable store always holds the newest tokens.
- Existing installs migrate on first boot the same way; a failing keychain
  write keeps the tokens in localStorage.
- If a shell is there but the keychain entry is missing or unreadable, the
  app shows Retry / Sign in again instead of logging out, and the mirror
  is not started (it would clear the entry).

Fixes #105

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared merged commit 45c3b16694 into lotus 2026-10-23 20:59:25 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#445