Skip to content

fix: persist API_SECRET + cross-device session restore (#259) - #260

Open
AmintaCCCP wants to merge 2 commits into
mainfrom
fix/persist-api-secret-259
Open

fix: persist API_SECRET + cross-device session restore (#259)#260
AmintaCCCP wants to merge 2 commits into
mainfrom
fix/persist-api-secret-259

Conversation

@AmintaCCCP

@AmintaCCCP AmintaCCCP commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #259 — the repeated prompts to re-enter the GitHub PAT and API_SECRET after a Docker restart / tab close. Per the reporter's (@raphaelbahat) request in the issue, API_SECRET is now persisted rather than cleared on tab close.

This PR also layers a code-audit pass on the changeset; the audit findings were applied as fixes within the same PR.

What changed

Client persistence

  • backendApiSecret is now persisted to IndexedDB (persist v10) and, additionally, written to a synchronous localStorage “auth mirror”. The mirror back-fills when the async IndexedDB unload-write never lands (the failure mode behind the bug). Persisted values always win over the mirror.
  • v9→v10 migration initializes backendApiSecret to null for older snapshots.

Cross-device session restore

  • New POST /api/sync/auth route (gated by the existing authMiddleware on /api/*) returns the stored, decrypted GitHub token so a fresh browser/device holding only API_SECRET can re-establish a session.
  • tryRestoreAuthFromBackend re-validates the restored token against the GitHub API before logging in, with re-entrancy (_isRestoringAuth) and double-check guards, and never clobbers an existing local session.

Audit fixes (applied here, not separate PR)

  1. logout() full credential teardown (security) — After persisting the secret, the old logout() left backendApiSecret in memory and in IndexedDB, so the backend kept authenticating a logged-out user. Now logout() clears the localStorage mirror, the sessionStorage cache, and the in-memory secret.
  2. /api/sync/auth Cache-Control: no-store — the response carries the decrypted PAT; defence-in-depth beyond the global helmet() default.
  3. Test coverage — logout test now asserts the memory + sessionStorage secret are torn down, not just the localStorage mirror.

Notes / non-blocking

  • The /api/sync/auth route deliberately echoes the PAT (unlike /sync/export which masks it) — the restore path needs the raw token to re-validate against the GitHub API. Echo is gated behind API_SECRET and documented in the route. No PAT is ever logged (verified against logSanitizer).

Test plan

Closes #259

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added automatic GitHub sign-in restoration from the backend during app startup and after a successful backend connection check.
    • Improved authentication persistence across refreshes and restarts.
  • Bug Fixes

    • Authentication data now clears more completely on logout.
    • Missing or unreadable stored credentials safely fall back to no session.
  • Tests

    • Added coverage for authentication restoration, persistence, and logout behavior.

Resolve the repeated PAT / API_SECRET re-prompt after Docker or tab restart
(#259, reporter raphaelbahat explicitly requested persisting API_SECRET).

Client persistence:
- Partialize backendApiSecret to IndexedDB (persist v10) and keep a synchronous
  localStorage "auth mirror" as a fallback when the async IndexedDB unload write
  never lands; persisted values always win over the mirror.
- v9→v10 migrate initializes backendApiSecret to null for older snapshots.

Cross-device restore:
- New POST /api/sync/auth route (API_SECRET-gated) returns the stored GitHub
  token; tryRestoreAuthFromBackend re-validates it against the GitHub API before
  logging in, with re-entrancy + double-check guards and a no-clobber rule for
  any existing local session.

Audit fixes layered on top:
- logout() now fully tears down backendApiSecret (memory + sessionStorage +
  localStorage mirror); previously it survived logout and got re-persisted,
  leaving the backend authenticating a logged-out user.
- /api/sync/auth sets Cache-Control: no-store on the PAT-bearing response
  (defence-in-depth beyond the global helmet() default).
- jsdom localStorage/sessionStorage shim in test/setup.ts for the auth mirror.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6556af24-9bf8-4130-aa34-bf8fb6d4a5d9

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4911c and 71f18ea.

📒 Files selected for processing (4)
  • src/App.tsx
  • src/store/useAppStore.test.ts
  • src/test/setup.ts
  • vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/store/useAppStore.test.ts
  • src/App.tsx
  • src/test/setup.ts

📝 Walkthrough

Walkthrough

The change adds a backend authentication restore endpoint, client-side session recovery, and a localStorage authentication mirror. Application startup and backend connection flows now restore GitHub sessions when local credentials are unavailable.

Changes

Authentication restoration

Layer / File(s) Summary
Authentication state persistence
src/store/useAppStore.ts, src/store/useAppStore.test.ts, src/test/setup.ts, vitest.config.ts
Authentication hydration now falls back to a localStorage mirror. Backend API secrets are persisted and synchronized. Logout clears all credential stores. Tests cover persistence, restoration, precedence, and cleanup.
Backend authentication endpoint
server/src/routes/authRestore.ts, server/src/index.ts, server/tests/routes/authRestore.test.ts
POST /api/sync/auth decrypts the stored GitHub token and returns it or null. Authentication responses disable caching. Tests cover retrieval, missing tokens, and decryption failures.
Client authentication restoration
src/services/backendAdapter.ts, src/services/autoSync.ts, src/App.tsx, src/components/settings/BackendPanel.tsx
The client requests and validates the backend token, updates the store, and invokes restoration during startup and after backend connection. Guards prevent duplicate or conflicting restoration attempts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant RestoreService
  participant BackendAdapter
  participant AuthRestoreRouter
  participant Database
  participant GitHubApiService
  participant AppStore
  App->>RestoreService: tryRestoreAuthFromBackend()
  RestoreService->>BackendAdapter: restoreAuth()
  BackendAdapter->>AuthRestoreRouter: POST /api/sync/auth
  AuthRestoreRouter->>Database: read and decrypt github_token
  Database-->>AuthRestoreRouter: token or null
  AuthRestoreRouter-->>BackendAdapter: github_token response
  RestoreService->>GitHubApiService: validate token
  GitHubApiService-->>RestoreService: GitHub user
  RestoreService->>AppStore: store token and user
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: persisting API_SECRET and restoring sessions across devices.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/persist-api-secret-259

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.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/App.tsx`:
- Around line 159-162: Update the startup flow in App so
tryRestoreAuthFromBackend() runs immediately after backend.init() and before
syncFromBackend(), instead of waiting for the backend data pull to finish. Keep
the existing backend initialization and session-recovery behavior intact, but
reorder the calls so auth restoration can complete before the app decides to
render LoginScreen.

In `@src/store/useAppStore.test.ts`:
- Line 319: Update the localStorage parsing line in useAppStore.test so parsed
is declared with const instead of let, since it is never reassigned. Keep the
JSON.parse and AUTH_MIRROR_KEY logic unchanged, and adjust only the declaration
in that test block to satisfy prefer-const.

In `@src/test/setup.ts`:
- Around line 56-60: Configure the jsdom test environment in vitest.config.ts
with an explicit non-opaque URL via test.environmentOptions.url, and remove the
storage-shim fallback around window.localStorage and window.sessionStorage in
setup.ts. Preserve normal jsdom storage behavior once the origin is configured.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d1e6b79-e9a1-4704-8f71-11306c8cb761

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef189f and 5e4911c.

📒 Files selected for processing (10)
  • server/src/index.ts
  • server/src/routes/authRestore.ts
  • server/tests/routes/authRestore.test.ts
  • src/App.tsx
  • src/components/settings/BackendPanel.tsx
  • src/services/autoSync.ts
  • src/services/backendAdapter.ts
  • src/store/useAppStore.test.ts
  • src/store/useAppStore.ts
  • src/test/setup.ts

Comment thread src/App.tsx Outdated
Comment thread src/store/useAppStore.test.ts Outdated
Comment thread src/test/setup.ts
- App.tsx: run tryRestoreAuthFromBackend before syncFromBackend so auth
  restoration completes before the app decides to render LoginScreen
- useAppStore.test.ts: use const for non-reassigned parsed var (prefer-const)
- vitest.config.ts: set non-opaque jsdom test URL via environmentOptions
- setup.ts: document why a guarded storage shim is still required (Node's
  experimental localStorage global shadows jsdom Storage on this runtime)
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.

[Bug] Repeated prompts to re-enter PAT and API_SECRET; values not persisted after Docker restart

1 participant