Skip to content

fix(desktop): prevent idle logout and memory leak in auth system - #202

Merged
2witstudios merged 1 commit into
masterfrom
claude/fix-idle-logout-issue-58qwe
Jan 14, 2026
Merged

2witstudios merged 1 commit into
masterfrom
claude/fix-idle-logout-issue-58qwe

Conversation

@2witstudios

@2witstudios 2witstudios commented Jan 14, 2026 •

Copy link
Copy Markdown
Owner

Root cause: Desktop was getting logged out overnight due to:

  1. Memory leak: IPC listeners for auth:cleared were never cleaned up, causing multiple callbacks to fire and corrupt auth state
  2. Missing refresh token: Desktop login never received a refresh token, so when device token refresh fell back to refresh token, it failed
  3. Premature logout: Network errors and rate limits triggered logout instead of being handled by retry logic

Changes:

  • preload/index.ts: Add cleanup function return from on() listener, add removeAllListeners() method for complete cleanup
  • auth-fetch.ts: Add initialized flag to prevent duplicate listeners, store and cleanup auth:cleared listener properly, make desktop refresh more robust - only logout on genuine 401 auth failure, handle network errors and rate limits gracefully
  • useAuthStore.ts: Store and cleanup IPC listener reference properly, use unified refresh flow for desktop, distinguish transient failures from genuine auth invalidation

The desktop app should now stay logged in for the full 90-day device token lifetime without being logged out due to overnight idle.

Summary by CodeRabbit

  • New Features

    • IPC listener API now returns a cleanup function to remove specific listeners.
    • Added removeAllListeners to clear all listeners on a channel.
  • Bug Fixes

    • Improved authentication error handling and retry/fallback behavior for desktop sessions.
    • Prevented listener duplication and memory leaks via explicit listener cleanup.
  • Refactor

    • Unified and simplified desktop token refresh flow and related state handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jan 14, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 8eb39a0 and 35b8381.

📒 Files selected for processing (3)
  • apps/desktop/src/preload/index.ts
  • apps/web/src/lib/auth/auth-fetch.ts
  • apps/web/src/stores/useAuthStore.ts
📝 Walkthrough

Walkthrough

Updated the Electron preload IPC API so on returns a cleanup function and added removeAllListeners; refactored desktop and web authentication flows to centralize refresh logic, add listener lifecycle management, and expand error handling for token refresh and session recovery.

Changes

Cohort / File(s) Summary
IPC Preload API
apps/desktop/src/preload/index.ts
on(channel, callback) now returns a () => void cleanup function that removes the specific listener; added removeAllListeners(channel); updated ElectronAPI type to reflect both changes.
Auth refresh & listener lifecycle
apps/web/src/lib/auth/auth-fetch.ts
Added listener initialization and cleanup (authClearedCleanup, initialized, initializeListeners()); reworked desktop refresh flow to require device token for long-lived sessions with a fallback to refresh token; centralized response handling, improved error handling for network/401/429/5xx, and added JWT cache sequencing.
Auth store & IPC cleanup
apps/web/src/stores/useAuthStore.ts
Replaced device-token-specific branches with unified refreshAuthSession flow; added _authClearedCleanup to manage IPC listener lifecycle; adjusted 401 handling to increment non-fatal failure counters and avoid forced logout on transient failures; cleaned up listener registration to avoid duplicates.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Init
    participant AuthFetch as AuthFetch
    participant AuthStore as AuthStore
    participant Desktop as Electron (ipcRenderer)
    participant API as Auth API

    App->>AuthFetch: new AuthFetch()
    AuthFetch->>AuthFetch: initializeListeners()
    AuthFetch->>Desktop: ipcRenderer.on('auth:cleared', handler)
    Desktop-->>AuthFetch: returns cleanup fn

    App->>AuthStore: initializeEventListeners()
    AuthStore->>Desktop: ipcRenderer.on('auth:cleared', handler)
    Desktop-->>AuthStore: returns cleanup fn stored in _authClearedCleanup

    Note over AuthStore,AuthFetch: Session load -> receives 401
    AuthStore->>AuthFetch: refreshAuthSession()
    AuthFetch->>AuthFetch: check deviceToken
    alt deviceToken available
        AuthFetch->>API: refresh with deviceToken
        API-->>AuthFetch: success / error
    else no deviceToken
        AuthFetch->>AuthStore: indicate re-auth required
    end
    AuthFetch-->>AuthStore: {success, shouldLogout}

    alt refresh success
        AuthStore->>AuthStore: clearJWTCache() and retry loadSession()
    else non-fatal failure
        AuthStore->>AuthStore: increment failedAuthAttempts, update lastFailedAuthCheck
    else fatal failure
        AuthStore->>AuthStore: logout()
    end

    Desktop->>AuthFetch: auth:cleared event
    AuthFetch->>AuthFetch: invoke cleanup as needed
    Desktop->>AuthStore: auth:cleared event
    AuthStore->>AuthStore: invoke _authClearedCleanup
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I twitch my nose and tidy threads,
I unhook listeners from their beds.
Tokens renewed or gently cleared,
No ghostly calls remain, I cheer!
Hoppy commits, all clean and dear 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing desktop auth issues (idle logout prevention and memory leak resolution) through listener cleanup and improved refresh logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/web/src/lib/auth/auth-fetch.ts (2)

44-61: Redundant cleanup check in constructor-only method.

Since initializeListeners() is only called from the constructor and guarded by initialized, the pre-existing listener cleanup on lines 47-50 will never execute (there cannot be an existing cleanup function on first initialization). This cleanup logic would only be useful if initializeListeners were called multiple times, but the initialized flag prevents that.

Consider whether this defensive cleanup is intentional for future-proofing, or if it can be simplified.


462-485: Consider logging the refresh token fallback result.

When the refresh token fallback succeeds (line 464-485), there's no success log message like there is for device token refresh (line 452). Adding a debug log would improve observability.

💡 Suggested improvement
           response = await fetch('/api/auth/mobile/refresh', {
             method: 'POST',
             headers: { 'Content-Type': 'application/json' },
             credentials: 'include',
             body: JSON.stringify({
               refreshToken,
               deviceToken,
               deviceId: deviceInfo.deviceId,
               platform: 'desktop',
             }),
           });
+
+          if (response.ok) {
+            this.logger.debug('Desktop: Refresh token fallback succeeded');
+          }
         } catch (networkError) {
apps/web/src/stores/useAuthStore.ts (1)

539-545: Web event listeners not cleaned up.

While IPC listeners now have cleanup, the window.addEventListener calls for auth:refreshed and auth:expired don't have corresponding cleanup stored. If initializeEventListeners is called multiple times (e.g., HMR), listeners could accumulate.

The removeEventListener calls on lines 540-541 mitigate this, but consider adding a similar cleanup pattern for consistency and to enable explicit teardown if needed.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9721bb0 and c7b8b10.

📒 Files selected for processing (3)
  • apps/desktop/src/preload/index.ts
  • apps/web/src/lib/auth/auth-fetch.ts
  • apps/web/src/stores/useAuthStore.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/stores/useAuthStore.ts
  • apps/web/src/lib/auth/auth-fetch.ts
  • apps/desktop/src/preload/index.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/stores/useAuthStore.ts
  • apps/web/src/lib/auth/auth-fetch.ts
  • apps/desktop/src/preload/index.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/stores/useAuthStore.ts
  • apps/web/src/lib/auth/auth-fetch.ts
  • apps/desktop/src/preload/index.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/stores/useAuthStore.ts
  • apps/web/src/lib/auth/auth-fetch.ts
**/*auth*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*auth*.{ts,tsx}: Use custom JWT authentication with jose library for user management
Use bcryptjs for password hashing

Files:

  • apps/web/src/lib/auth/auth-fetch.ts
🧬 Code graph analysis (1)
apps/web/src/stores/useAuthStore.ts (1)
apps/web/src/lib/auth/auth-fetch.ts (4)
  • refreshAuthSession (545-547)
  • refreshAuthSession (787-788)
  • clearJWTCache (640-642)
  • clearJWTCache (784-785)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (11)
apps/web/src/lib/auth/auth-fetch.ts (6)

32-42: LGTM! Good singleton initialization guard.

The initialized flag properly prevents duplicate listener registration during HMR or multiple imports. This addresses the memory leak root cause mentioned in the PR objectives.


428-433: Good guard for missing device token on desktop.

This correctly identifies when desktop needs re-authentication (no device token) and returns an appropriate shouldLogout: true result, aligning with the PR goal of distinguishing genuine auth failures from transient issues.


437-460: LGTM! Network error handling prevents false logouts.

The try/catch around the device token refresh correctly catches network errors and returns shouldLogout: false, preventing the desktop app from logging out due to transient network issues. This addresses a key issue in the PR objectives.


487-519: Well-structured response status handling.

The cascading checks properly distinguish between:

  • 401: Genuine auth failure → logout
  • 429: Rate limited → don't logout, retry later
  • 5xx: Server error → don't logout, retry later
  • Other failures → don't logout

This matches the PR goal of only logging out on genuine authentication failures.


280-300: Potential unhandled rejection in queued request processing.

The queue.forEach(async ...) pattern creates fire-and-forget promises. If any queued request fails, the rejection is caught and logged, but the overall refreshToken() method won't await these. This is likely intentional for parallelism, but ensure callers don't depend on queued requests completing before refreshToken() returns.


52-60: The code safely handles the optional chaining; window.electron.on always returns a cleanup function.

The preload implementation confirms that the on method (lines 32-37 of apps/desktop/src/preload/index.ts) always returns a cleanup function with type () => void. The if (cleanup) check in your code properly guards against the case where the optional chaining short-circuits (if window.electron or the on method doesn't exist), making this implementation correct.

apps/desktop/src/preload/index.ts (2)

32-42: LGTM! Proper IPC listener lifecycle management.

The on method now correctly:

  1. Captures the handler reference
  2. Registers it with ipcRenderer.on
  3. Returns a cleanup function that removes only that specific listener

The new removeAllListeners method provides a nuclear option for complete channel cleanup. This directly addresses the memory leak issue from the PR.


99-100: Type definitions correctly reflect the API changes.

The on return type is now () => void (cleanup function) and removeAllListeners is properly declared.

apps/web/src/stores/useAuthStore.ts (3)

283-309: Improved desktop token refresh flow with proper failure distinction.

The unified refresh flow correctly:

  1. Uses refreshAuthSession() which handles device tokens properly
  2. Clears JWT cache before retry
  3. Distinguishes fatal vs non-fatal failures via shouldLogout
  4. Records failed attempts without clearing user state for transient failures

This aligns with the auth-fetch.ts changes and addresses the idle logout issue.


456-458: Non-private property exposed on helpers object.

_authClearedCleanup is prefixed with underscore suggesting it's internal, but it's directly on the exported authStoreHelpers object. This is consistent with _authPromise and _serverSessionInitialized in the store, so the pattern is acceptable, but the naming convention matches.


548-560: LGTM! Proper IPC listener cleanup prevents memory leaks.

The cleanup pattern correctly:

  1. Cleans up existing listener before adding new one
  2. Nullifies the cleanup reference after calling it
  3. Stores the new cleanup function for future use
  4. Safely handles the optional chaining on window.electron.on?.

This directly addresses the memory leak root cause from the PR objectives.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread apps/web/src/stores/useAuthStore.ts
@2witstudios
2witstudios force-pushed the claude/fix-idle-logout-issue-58qwe branch from c7b8b10 to 8eb39a0 Compare January 14, 2026 15:09
Root cause: Desktop was getting logged out overnight due to:
1. Memory leak: IPC listeners for auth:cleared were never cleaned up,
   causing multiple callbacks to fire and corrupt auth state
2. Missing refresh token: Desktop login never received a refresh token,
   so when device token refresh fell back to refresh token, it failed
3. Premature logout: Network errors and rate limits triggered logout
   instead of being handled by retry logic

Changes:
- preload/index.ts: Add cleanup function return from on() listener,
  add removeAllListeners() method for complete cleanup
- auth-fetch.ts: Add initialized flag to prevent duplicate listeners,
  store and cleanup auth:cleared listener properly, make desktop
  refresh more robust - only logout on genuine 401 auth failure,
  handle network errors and rate limits gracefully
- useAuthStore.ts: Store and cleanup IPC listener reference properly,
  use unified refresh flow for desktop, distinguish transient failures
  from genuine auth invalidation

The desktop app should now stay logged in for the full 90-day device
token lifetime without being logged out due to overnight idle.
@2witstudios
2witstudios force-pushed the claude/fix-idle-logout-issue-58qwe branch from 8eb39a0 to 35b8381 Compare January 14, 2026 15:13
@2witstudios
2witstudios merged commit 6dc1828 into master Jan 14, 2026
7 checks passed
@2witstudios
2witstudios deleted the claude/fix-idle-logout-issue-58qwe branch January 29, 2026 02:23
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.

2 participants