Desktop: mirror the login tokens into the OS keychain (#105, step 1) #254

Merged
jared merged 1 commits from desktop-keychain-mirror into lotus 2026-09-29 09:34:58 -04:00
Owner

Step 1 of #105 (the plan in the issue): the desktop app keeps a verified copy of the login tokens in the OS keychain (Windows Credential Manager).

The session is still read from localStorage exactly as before, so nothing about login changes, and a keychain problem can't log anyone out.

Step 2 comes in a later release, once this has run on real installs. It switches reads to the keychain and removes the tokens from localStorage. That's the step that actually takes the tokens off disk.

Changes

  • sessions.ts: new onSessionPersisted listeners, called on every write (login, token rotation) and on removal (logout). A throwing listener can't break the write.
  • keychainMirror.ts (desktop only):
    • It mirrors only userId, deviceId, accessToken and refreshToken.
    • It reads first and writes only when the stored copy differs, then verifies by reading it back.
    • Writes are serialized, with 5 s timeouts.
    • When there's no session it clears the keychain. That also covers a logout whose page reload beat the clear.
    • Every failure becomes a status, never an exception.
  • Settings → General (desktop): a "Login in the system keychain" line showing the status.
  • Safe to merge first: a desktop build without the new commands reads as unsupported and shows no error. So this can ship before or after the desktop PR.

Tested

  • Unit tests (fake keychain):
    • store, no rewrite when already current, token rotation, clear;
    • unsupported, missing commands, denied write, read-back mismatch, timeout;
    • the session listener hook.
  • Simulated desktop app, 14/14:
scenario result
login only the secrets are mirrored; the session stays in localStorage
Settings "A copy of your login is kept in the system keychain (Windows Credential Manager)…"
reload verified without rewriting
logout keychain cleared
older desktop build → upgrade nothing and no error; then the existing session is mirrored at startup
Linux / "Access is denied." stays logged in; honest status line
  • Real Linux desktop binary: the commands answer unsupported, login is unaffected, and Settings says so.
  • Suites: unit 1,295 pass; Playwright 20 passed.

Not tested here: the real Windows Credential Manager. That's covered by the Windows test on the desktop PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA

**Step 1 of #105** (the plan in the issue): the desktop app keeps a **verified copy** of the login tokens in the OS keychain (Windows Credential Manager). The session is **still read from localStorage exactly as before**, so nothing about login changes, and a keychain problem can't log anyone out. **Step 2** comes in a later release, once this has run on real installs. It switches reads to the keychain and removes the tokens from localStorage. That's the step that actually takes the tokens off disk. ## Changes - **`sessions.ts`:** new `onSessionPersisted` listeners, called on every write (login, token rotation) and on removal (logout). A throwing listener can't break the write. - **`keychainMirror.ts`** (desktop only): - It mirrors only `userId`, `deviceId`, `accessToken` and `refreshToken`. - It reads first and writes only when the stored copy differs, then verifies by reading it back. - Writes are serialized, with 5 s timeouts. - When there's no session it clears the keychain. That also covers a logout whose page reload beat the clear. - Every failure becomes a status, never an exception. - **Settings → General (desktop):** a "Login in the system keychain" line showing the status. - **Safe to merge first:** a desktop build without the new commands reads as *unsupported* and shows no error. So this can ship before or after the desktop PR. ## Tested - **Unit tests (fake keychain):** - store, no rewrite when already current, token rotation, clear; - unsupported, missing commands, denied write, read-back mismatch, timeout; - the session listener hook. - **Simulated desktop app, 14/14:** | scenario | result | |---|---| | login | only the secrets are mirrored; the session stays in localStorage | | Settings | "A copy of your login is kept in the system keychain (Windows Credential Manager)…" | | reload | verified without rewriting | | logout | keychain cleared | | older desktop build → upgrade | nothing and no error; then the existing session is mirrored at startup | | Linux / "Access is denied." | stays logged in; honest status line | - **Real Linux desktop binary:** the commands answer *unsupported*, login is unaffected, and Settings says so. - **Suites:** unit 1,295 pass; Playwright 20 passed. **Not tested here:** the real Windows Credential Manager. That's covered by the Windows test on the desktop PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
jared added 1 commit 2026-09-29 00:25:23 -04:00
feat(desktop): mirror the login tokens into the OS keychain (#105, step 1)
CI / Build & Quality Checks (pull_request) Successful in 1m46s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 8m44s
9b9f33b270
Step 1 of moving the desktop app's login out of the webview's plaintext
localStorage: keep a verified copy of the tokens in the OS keychain
(Windows Credential Manager, via cinny-desktop's new secure_session_*
commands). The session is still read from localStorage exactly as before,
so nothing about login changes and a keychain problem can't log anyone out.
Step 2 (a later release, once this has run on real installs) switches reads
to the keychain and drops the tokens from localStorage.

- sessions.ts: onSessionPersisted — listeners told about every session
  write (login, token rotation) and removal (logout); a throwing listener
  can't break the write.
- keychainMirror.ts: desktop only. Mirrors userId/deviceId/accessToken/
  refreshToken (not the rest of the session); reads first and writes only
  when the copy differs, then verifies by reading back; serialized, 5 s
  timeouts; no session → clear (also covers a logout whose reload beat the
  clear). Every failure is a status, never an exception. A desktop build
  without the commands reads as "unsupported", so this can ship before the
  desktop side.
- Settings → General (desktop): "Login in the system keychain" status.

Tested: unit tests (fake keychain: store, no rewrite when current, rotation,
clear, unsupported, missing commands, denied write, read-back mismatch,
timeout); a simulated desktop app with a fake keychain, 14/14 (login
mirrors only the secrets, Settings status, reload verifies without
rewriting, logout clears, an existing session is mirrored after upgrade,
Linux/denied show an honest status and stay logged in); the real Linux
desktop binary (commands answer "unsupported", login unaffected, Settings
says so). Unit 1295 pass, Playwright 20 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
jared merged commit d6548c56fd into lotus 2026-09-29 09:34:58 -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#254