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
@@ -233,6 +233,27 @@ export type SessionStoreName = {
|
||||
// crypto: 'crypto-store',
|
||||
// } as const;
|
||||
|
||||
// [Gitea #105] Listeners told about every session write in THIS tab (login,
|
||||
// token rotation) and removal (logout), e.g. the desktop keychain mirror.
|
||||
// A throwing listener never breaks the write.
|
||||
type PersistListener = (session: Session | null) => void;
|
||||
const persistListeners = new Set<PersistListener>();
|
||||
const notifyPersisted = (session: Session | null): void => {
|
||||
persistListeners.forEach((cb) => {
|
||||
try {
|
||||
cb(session);
|
||||
} catch {
|
||||
/* listener errors must not affect login/logout */
|
||||
}
|
||||
});
|
||||
};
|
||||
export const onSessionPersisted = (cb: PersistListener): (() => void) => {
|
||||
persistListeners.add(cb);
|
||||
return () => {
|
||||
persistListeners.delete(cb);
|
||||
};
|
||||
};
|
||||
|
||||
// Persist the session. Writes the atomic blob FIRST (so the consistent,
|
||||
// never-torn copy is established before the multi-key legacy write), then
|
||||
// dual-writes the legacy keys for rollback safety. Signature is unchanged —
|
||||
@@ -249,6 +270,7 @@ export function setFallbackSession(
|
||||
localStorage.setItem(SESSION_BLOB_KEY, JSON.stringify(persisted));
|
||||
// Dual-write the legacy keys (removal of this half is a future release).
|
||||
writeLegacyKeys(persisted);
|
||||
notifyPersisted(sessionFromPersisted(persisted));
|
||||
}
|
||||
|
||||
// Clear BOTH the atomic blob and every legacy key so no reader (blob-preferring
|
||||
@@ -257,6 +279,7 @@ export const removeFallbackSession = () => {
|
||||
localStorage.removeItem(SESSION_BLOB_KEY);
|
||||
Object.values(LEGACY_KEYS).forEach((key) => localStorage.removeItem(key));
|
||||
Object.values(OIDC_KEYS).forEach((key) => localStorage.removeItem(key));
|
||||
notifyPersisted(null);
|
||||
};
|
||||
|
||||
// Read the session, preferring the atomic blob. If the blob is absent or
|
||||
|
||||
Reference in New Issue
Block a user