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
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
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
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
91f82d60e3
commit
9b9f33b270
@@ -432,3 +432,20 @@ test('subscribeSessionChanges ignores unrelated storage keys', () => {
|
||||
listeners.forEach((cb) => cb({ key: 'some_unrelated_preference' }));
|
||||
assert.equal(fired, false);
|
||||
});
|
||||
|
||||
test('onSessionPersisted: told about writes and removals; a throwing listener is harmless', async () => {
|
||||
installStorage();
|
||||
const { onSessionPersisted } = await import('./sessions');
|
||||
const seen: (string | null)[] = [];
|
||||
const offBad = onSessionPersisted(() => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
const off = onSessionPersisted((s) => seen.push(s ? s.accessToken : null));
|
||||
setFallbackSession('tok-a', 'DEV', '@a:hs', 'https://hs', { refreshToken: 'ref-a' });
|
||||
removeFallbackSession();
|
||||
off();
|
||||
offBad();
|
||||
setFallbackSession('tok-b', 'DEV', '@a:hs', 'https://hs');
|
||||
assert.deepEqual(seen, ['tok-a', null]);
|
||||
assert.equal(getFallbackSession()?.accessToken, 'tok-b', 'write still happened');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user