Skip to content

fix(auth): offer "clear app data" recovery when login decryption fails#1652

Merged
senamakel merged 7 commits into
tinyhumansai:mainfrom
senamakel:fix/login-decryption-recovery
May 13, 2026
Merged

fix(auth): offer "clear app data" recovery when login decryption fails#1652
senamakel merged 7 commits into
tinyhumansai:mainfrom
senamakel:fix/login-decryption-recovery

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 13, 2026

Summary

  • Detect "Decryption failed" errors during the OAuth deep-link sign-in flow and surface a one-click Clear app data & restart recovery directly in the Welcome screen.
  • Extract the existing Settings → Danger Zone clear-data sequence (CEF profile purge → core workspace reset → redux-persist + window storage wipe → app restart) into a shared clearAllAppData() util used by both call sites.
  • Plumb a requiresAppDataReset flag through DeepLinkAuthState so the Welcome screen only shows the destructive action when it's actually applicable.

Problem

When the local core sidecar can't decrypt secrets at sign-in (e.g. encryption key rotated, profile copied between machines, tampered/corrupt storage), the deep-link auth handler used to swallow the underlying Decryption failed — wrong key or tampered data error and surface a generic "Sign-in failed. Please try again." with no path forward. The Settings → "Clear App Data" escape hatch is unreachable while logged out, so users were effectively stuck on the Welcome screen.

Solution

  • desktopDeepLinkListener.ts inspects the caught error from handleAuthDeepLink for decryption failed / wrong key or tampered data / corrupt data substrings. On match, it flips the new requiresAppDataReset flag on DeepLinkAuthState and shows a more useful message ("Clear app data to start fresh" + reassurance the cloud account is untouched).
  • Welcome.tsx reads the flag and renders a red "Clear app data & restart" button inside the existing error block, with an in-flight spinner and inline failure surface.
  • Extracted app/src/utils/clearAllAppData.ts (CEF profile purge → optional core clearSession → workspace reset/core restart → redux-persist + window storage wipe → app restart). Welcome calls it without clearSession (no live session pre-login); SettingsHome.tsx calls it with the current user's session clear callback. Both call sites stay in sync.
  • Tests cover the new option flag in deepLinkAuthState, the existing snapshot tests, and the listener's error-classification path.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • N/A: behaviour-only change — no new/removed/renamed feature rows in the coverage matrix
  • N/A: no feature IDs from the matrix apply to this UX-recovery change
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: does not touch release-cut surfaces (deep-link auth error path only, manual smoke unchanged)
  • N/A: no linked issue

Impact

  • Desktop only. The deep-link auth path and clearAllAppData are Tauri-specific; web/CLI surfaces are untouched.
  • No migration required. The new requiresAppDataReset field defaults to false and is recomputed on every transition.
  • User-visible behavior: when (and only when) sign-in fails because of a local decryption error, a destructive recovery action appears on the Welcome screen. Cloud account / server-side data is unaffected.
  • Security: the recovery wipes local secrets and the CEF profile; matches the Settings → Danger Zone behavior bit-for-bit (same util).

Related

  • Closes:
  • Follow-up PR(s)/TODOs:

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/login-decryption-recovery
  • Commit SHA: 1b7a086

Validation Run

  • `pnpm --filter openhuman-app format:check`
  • `pnpm typecheck`
  • Focused tests: `pnpm debug unit deepLinkAuthState desktopDeepLinkListener SettingsHome OAuthProviderButton Welcome` — 83 passed
  • N/A: Rust fmt/check (if changed): no Rust changes
  • N/A: Tauri fmt/check (if changed): no Tauri shell changes

Validation Blocked

  • `command:` N/A
  • `error:` N/A
  • `impact:` N/A

Behavior Changes

  • Intended behavior change: surface a "Clear app data & restart" button on the Welcome screen when login fails due to a decryption error.
  • User-visible effect: previously stuck users now have an in-product recovery path; happy-path sign-in is unchanged.

Parity Contract

  • Legacy behavior preserved: non-decryption deep-link auth failures still render the existing generic "Sign-in failed. Please try again." message with no recovery button.
  • Guard/fallback/dispatch parity checks: `requiresAppDataReset` defaults to `false` on every transition (`begin`/`complete`), so stale state can't leave the button visible after a successful retry.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution: N/A

Summary by CodeRabbit

  • New Features

    • Centralized “Clear app data & restart” flow added to Settings and Welcome with confirmation, loading state, and error feedback.
  • Bug Fixes

    • Better detection of local decryption/key-mismatch failures and now surface a targeted recovery option to wipe local data and restart.
  • Tests

    • Expanded test coverage for the reset hint, auth-state transitions, and the clear-app-data workflow.

Review Change Stack

senamakel added 2 commits May 13, 2026 07:43
When the core sidecar fails to decrypt locally stored secrets during
sign-in (key rotated, profile copied across machines, tampered storage),
the deep-link auth flow used to surface a generic "Sign-in failed.
Please try again." with no path forward. The user had to know about the
hidden Settings → Danger Zone option, which is unreachable while logged
out.

Detect the decryption error in the deep-link handler and expose a
"Clear app data & restart" button directly in the Welcome screen's
error block. Extracts the existing Settings clear-app-data logic into
a shared util so both call sites stay in sync.
@senamakel senamakel requested a review from a team May 13, 2026 14:46
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54e15b4d-0bcd-474a-940e-7a0046870e76

📥 Commits

Reviewing files that changed from the base of the PR and between 89911c3 and 748b56e.

📒 Files selected for processing (3)
  • app/src/components/settings/__tests__/SettingsHome.test.tsx
  • app/src/pages/__tests__/Welcome.test.tsx
  • app/src/utils/__tests__/desktopDeepLinkListener.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/components/settings/tests/SettingsHome.test.tsx
  • app/src/pages/tests/Welcome.test.tsx

📝 Walkthrough

Walkthrough

Detect decryption/key-mismatch failures during deep-link OAuth auth, set a new requiresAppDataReset flag on failure, add a centralized clearAllAppData utility, and surface a destructive “Clear app data & restart” UI in Welcome/Settings wired to that utility.

Changes

App data reset on decryption failure

Layer / File(s) Summary
Deep-link auth state contract
app/src/store/deepLinkAuthState.ts, app/src/store/__tests__/deepLinkAuthState.test.ts
DeepLinkAuthState adds requiresAppDataReset: boolean; beginDeepLinkAuthProcessing, completeDeepLinkAuthProcessing, and failDeepLinkAuthProcessing(message, { requiresAppDataReset? }) manage the flag; tests assert full state shape across transitions.
Decryption detection in listener
app/src/utils/desktopDeepLinkListener.ts
handleAuthDeepLink normalizes caught errors, uses isDecryptionFailure() substring checks to detect local decryption/key-mismatch issues, and calls failDeepLinkAuthProcessing with { requiresAppDataReset: true } when detected.
Centralized app data clearing
app/src/utils/clearAllAppData.ts, app/src/utils/__tests__/clearAllAppData.test.ts
New clearAllAppData(options?: ClearAllAppDataOptions) orchestrates queueing a per-user CEF profile purge, optional clearSession, workspace/core reset, redux-persist purge, storage clears, and desktop restart; tests cover success, best-effort failures, and unrecoverable failures.
Welcome page clear data UI
app/src/pages/Welcome.tsx, app/src/pages/__tests__/Welcome.test.tsx
Imports clearAllAppData, adds isClearingAppData/resetError state, implements handleClearAppData and renders a destructive “Clear app data & restart” button (disabled/loading while running) when requiresAppDataReset is true; tests mock useDeepLinkAuthState and clearAllAppData.
Settings page logout flow
app/src/components/settings/SettingsHome.tsx, app/src/components/settings/__tests__/SettingsHome.test.tsx
Refactors handleLogoutAndClearData to call shared clearAllAppData({ clearSession, userId }); tests assert modal confirmation and that clearAllAppData is called with userId:null and a clearSession function.
OAuth provider button & listener tests
app/src/components/oauth/__tests__/OAuthProviderButton.test.tsx, app/src/utils/__tests__/desktopDeepLinkListener.test.ts
OAuthProviderButton tests updated to include requiresAppDataReset in mocked deep-link auth state; desktop listener tests added to assert requiresAppDataReset is set for decryption-related persistence failures and not set for other errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • graycyrus

Poem

🐰 In the burrow where keystores hide,
I tidy crumbs with hop and pride.
When keys refuse and errors sing,
I clear the stash and start the spring.
Reboot, reboot — and back we glide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: detecting decryption failures during OAuth sign-in and offering a recovery option, which is the primary objective of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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)
app/src/store/deepLinkAuthState.ts (1)

51-54: ⚡ Quick win

Extract failDeepLinkAuthProcessing options into a named interface.

Line 53 defines an inline object shape; prefer a dedicated interface for this options contract.

Suggested refactor
+interface FailDeepLinkAuthProcessingOptions {
+  requiresAppDataReset?: boolean;
+}
+
 export const failDeepLinkAuthProcessing = (
   message: string,
-  options: { requiresAppDataReset?: boolean } = {}
+  options: FailDeepLinkAuthProcessingOptions = {}
 ): void => {

As per coding guidelines "Prefer interface for defining object shapes in TypeScript".

🤖 Prompt for 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.

In `@app/src/store/deepLinkAuthState.ts` around lines 51 - 54, Extract the inline
options object into a named interface: create an interface (e.g.,
DeepLinkAuthFailOptions) that declares requiresAppDataReset?: boolean, update
the function signature of failDeepLinkAuthProcessing to use that interface
(options: DeepLinkAuthFailOptions = {}), and update any related types/usages to
reference DeepLinkAuthFailOptions so the options shape is defined via an
interface rather than an inline type.
app/src/utils/clearAllAppData.ts (1)

42-43: ⚡ Quick win

Use namespaced debug logging instead of console.warn in this reset path.

Line 42 and Line 52 use raw console logging. Please switch to a namespaced debug logger (dev-focused detail) for consistency with app logging policy.

As per coding guidelines "Use namespaced debug logs in TypeScript (e.g., via the debug npm package with namespace per area) plus dev-only detail where useful in app/src".

Also applies to: 52-53

🤖 Prompt for 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.

In `@app/src/utils/clearAllAppData.ts` around lines 42 - 43, Replace the raw
console.warn calls in the clearAllAppData reset path with a namespaced debug
logger: import and instantiate the debug logger for this module (e.g., namespace
like "app:clearAllAppData") and use it instead of console.warn in the error
handling in the clearAllAppData function (the two occurrences currently logging
"[clearAllAppData] Failed to queue CEF profile purge:" and the other at lines
~52-53). Ensure the debug calls include the error object and preserve the
existing message text so devs can enable the specific "app:clearAllAppData"
namespace for detailed output.
app/src/pages/__tests__/Welcome.test.tsx (1)

93-97: ⚡ Quick win

Extract a shared deep-link auth mock helper to avoid repeated state literals.

The new field had to be updated in many places; centralizing this mock setup will reduce future drift and test maintenance cost.

♻️ Proposed refactor
+const defaultDeepLinkAuthState = {
+  isProcessing: false,
+  errorMessage: null,
+  requiresAppDataReset: false,
+};
+
+const mockDeepLinkAuthState = (
+  overrides: Partial<typeof defaultDeepLinkAuthState> = {},
+) => {
+  vi.mocked(useDeepLinkAuthState).mockReturnValue({
+    ...defaultDeepLinkAuthState,
+    ...overrides,
+  });
+};

 beforeEach(() => {
   oauthButtonSpy.mockReset();
   oauthOverrideSpy.mockReset();
-  vi.mocked(useDeepLinkAuthState).mockReturnValue({
-    isProcessing: false,
-    errorMessage: null,
-    requiresAppDataReset: false,
-  });
+  mockDeepLinkAuthState();
   vi.mocked(clearCoreRpcUrlCache).mockReset();
   vi.mocked(clearBackendUrlCache).mockReset();
   vi.mocked(storeRpcUrl).mockReset();
 });
🤖 Prompt for 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.

In `@app/src/pages/__tests__/Welcome.test.tsx` around lines 93 - 97, Several tests
in Welcome.test.tsx duplicate the same deep-link auth state literal; extract a
single helper (e.g., createDeepLinkAuthMock or buildDeepLinkAuthState) that
returns the canonical mock object and replace inline literals in tests with
calls to that helper; update imports/uses in any describe/it blocks and any
helpers that assert against the deep-link auth shape (e.g., where deepLinkAuth,
mockDeepLinkState, or similar identifiers are referenced) so future field
additions only require updating the helper.
🤖 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 `@app/src/pages/Welcome.tsx`:
- Line 38: Replace the raw console.error call in the Welcome component
(Welcome.tsx) with the app’s namespaced debug logger (e.g., create/get a logger
for "app:welcome") and log only sanitized error details — for example
err.message and err.code or a trimmedMessage — rather than the full err object;
ensure the call is gated by the debug/logger’s enabled/dev-only checks so
PII/secrets are not emitted in production and remove any usage of the raw err
object from the log call.

---

Nitpick comments:
In `@app/src/pages/__tests__/Welcome.test.tsx`:
- Around line 93-97: Several tests in Welcome.test.tsx duplicate the same
deep-link auth state literal; extract a single helper (e.g.,
createDeepLinkAuthMock or buildDeepLinkAuthState) that returns the canonical
mock object and replace inline literals in tests with calls to that helper;
update imports/uses in any describe/it blocks and any helpers that assert
against the deep-link auth shape (e.g., where deepLinkAuth, mockDeepLinkState,
or similar identifiers are referenced) so future field additions only require
updating the helper.

In `@app/src/store/deepLinkAuthState.ts`:
- Around line 51-54: Extract the inline options object into a named interface:
create an interface (e.g., DeepLinkAuthFailOptions) that declares
requiresAppDataReset?: boolean, update the function signature of
failDeepLinkAuthProcessing to use that interface (options:
DeepLinkAuthFailOptions = {}), and update any related types/usages to reference
DeepLinkAuthFailOptions so the options shape is defined via an interface rather
than an inline type.

In `@app/src/utils/clearAllAppData.ts`:
- Around line 42-43: Replace the raw console.warn calls in the clearAllAppData
reset path with a namespaced debug logger: import and instantiate the debug
logger for this module (e.g., namespace like "app:clearAllAppData") and use it
instead of console.warn in the error handling in the clearAllAppData function
(the two occurrences currently logging "[clearAllAppData] Failed to queue CEF
profile purge:" and the other at lines ~52-53). Ensure the debug calls include
the error object and preserve the existing message text so devs can enable the
specific "app:clearAllAppData" namespace for detailed output.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75f2eabd-0330-479c-8c27-039bea1e32bd

📥 Commits

Reviewing files that changed from the base of the PR and between 9160317 and 1b7a086.

📒 Files selected for processing (9)
  • app/src/components/oauth/__tests__/OAuthProviderButton.test.tsx
  • app/src/components/settings/SettingsHome.tsx
  • app/src/pages/Welcome.tsx
  • app/src/pages/__tests__/Welcome.test.tsx
  • app/src/store/__tests__/deepLinkAuthState.test.ts
  • app/src/store/deepLinkAuthState.ts
  • app/src/utils/__tests__/desktopDeepLinkListener.test.ts
  • app/src/utils/clearAllAppData.ts
  • app/src/utils/desktopDeepLinkListener.ts

Comment thread app/src/pages/Welcome.tsx Outdated
…ilure

Per CodeRabbit + frontend logging guideline: replace raw `console.error`
with `debug('app:welcome')` and log a sanitized error message rather
than the full error object.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
senamakel added 2 commits May 13, 2026 08:16
…l path

Adds focused unit tests for the diff-coverage gate:

- clearAllAppData: full sequence, best-effort skips on CEF/clearSession
  failure, abort on workspace reset failure, default `userId=null`.
- Welcome: renders/hides the recovery button via `requiresAppDataReset`,
  invokes clearAllAppData on click, surfaces inline error on failure.
- desktopDeepLinkListener: decryption-error branch flips the new flag and
  rewrites the message; non-decryption failures stay generic.
- SettingsHome: confirms the Danger Zone flow forwards `clearSession` +
  current `userId` to the shared util.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/components/settings/__tests__/SettingsHome.test.tsx (1)

265-267: ⚡ Quick win

Avoid DOM-order coupling when selecting the confirm button.

Using “last button wins” is brittle. Scope the lookup to the modal/dialog so this stays stable if button order changes.

Suggested refactor
+import { render, screen, within } from '@testing-library/react';
...
-const confirmButtons = screen.getAllByRole('button', { name: /Clear App Data/i });
-// The last one is the modal confirm button (first is the menu item we just clicked).
-await user.click(confirmButtons[confirmButtons.length - 1]);
+const dialog = await screen.findByRole('dialog');
+await user.click(within(dialog).getByRole('button', { name: /Clear App Data/i }));
🤖 Prompt for 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.

In `@app/src/components/settings/__tests__/SettingsHome.test.tsx` around lines 265
- 267, The test currently finds all buttons named /Clear App Data/i and clicks
the last one (confirmButtons[confirmButtons.length - 1]), which couples it to
DOM order; instead locate the modal/dialog element after opening the menu (e.g.,
using screen.getByRole('dialog') or similar) and scope the query with
within(dialog).getByRole('button', { name: /Clear App Data/i }) so you click the
confirm button inside the modal — update the code in SettingsHome.test.tsx to
replace the global getAllByRole lookup and indexed click with a scoped query and
click using user.click on the modal-scoped button.
🤖 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 `@app/src/components/settings/__tests__/SettingsHome.test.tsx`:
- Around line 43-46: The top-level mockClearAllAppData is referenced inside the
vi.mock factory and must be hoisted to avoid TDZ; replace its declaration with a
hoisted wrapper using vi.hoisted to return the mock (keep the same mock behavior
of resolved undefined) and continue to reference mockClearAllAppData in the
vi.mock('../../../utils/clearAllAppData', ...) factory so the mock factory runs
after the hoisted initializer; target the mockClearAllAppData symbol and the
vi.mock call in SettingsHome.test.tsx when applying the change.

In `@app/src/utils/__tests__/desktopDeepLinkListener.test.ts`:
- Line 84: Replace the flaky setTimeout-based waits in the test with explicit
waiting on the deepLinkAuthState subscription: remove the await new
Promise(resolve => setTimeout(resolve, 0)) calls and instead create a Promise
that subscribes via subscribeDeepLinkAuthState, resolves when deepLinkAuthState
reaches the expected value, then unsubscribes; await that Promise in the test
before asserting. Target the test logic that triggers state changes and use the
subscription-based wait for determinism (use deepLinkAuthState and
subscribeDeepLinkAuthState identifiers).

---

Nitpick comments:
In `@app/src/components/settings/__tests__/SettingsHome.test.tsx`:
- Around line 265-267: The test currently finds all buttons named /Clear App
Data/i and clicks the last one (confirmButtons[confirmButtons.length - 1]),
which couples it to DOM order; instead locate the modal/dialog element after
opening the menu (e.g., using screen.getByRole('dialog') or similar) and scope
the query with within(dialog).getByRole('button', { name: /Clear App Data/i })
so you click the confirm button inside the modal — update the code in
SettingsHome.test.tsx to replace the global getAllByRole lookup and indexed
click with a scoped query and click using user.click on the modal-scoped button.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5631e768-33a5-4a29-b0dd-f032d6f70df6

📥 Commits

Reviewing files that changed from the base of the PR and between acac779 and 89911c3.

📒 Files selected for processing (4)
  • app/src/components/settings/__tests__/SettingsHome.test.tsx
  • app/src/pages/__tests__/Welcome.test.tsx
  • app/src/utils/__tests__/clearAllAppData.test.ts
  • app/src/utils/__tests__/desktopDeepLinkListener.test.ts

Comment thread app/src/components/settings/__tests__/SettingsHome.test.tsx Outdated
Comment thread app/src/utils/__tests__/desktopDeepLinkListener.test.ts Outdated
senamakel added 2 commits May 13, 2026 08:28
- Wrap `mockClearAllAppData` in `vi.hoisted(...)` in Welcome and
  SettingsHome tests so the symbol is initialized before `vi.mock`'s
  hoisted factory runs (avoids TDZ ReferenceError under Vitest v4).
- Replace `setTimeout(resolve, 0)` waits in the deep-link listener tests
  with a subscription-driven `waitForAuthSettled` helper that resolves
  when `deepLinkAuthState.isProcessing` flips false. Deterministic and
  doesn't rely on scheduler variance.
@senamakel senamakel merged commit 74a08c8 into tinyhumansai:main May 13, 2026
20 checks passed
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.

1 participant