Skip to content

feat: production wiring for encryption disable/rotate + recovery UX (Phase 4, #338 items 1+2) - #342

Merged
qnbs merged 11 commits into
mainfrom
feat/encryption-migration-phase4-b
Aug 13, 2026
Merged

feat: production wiring for encryption disable/rotate + recovery UX (Phase 4, #338 items 1+2)#342
qnbs merged 11 commits into
mainfrom
feat/encryption-migration-phase4-b

Conversation

@qnbs

@qnbsqnbs commented Aug 12, 2026

Copy link
Copy Markdown
Owner

User description

Summary

Completes issue #338's Phase 4 by wiring the previously test-only encryption migration
journal into real production entry points for disabling at-rest encryption and rotating
the passphrase. Stacked on #339 (merged), which already fixed the write-vs-migration race
this PR's migrations depend on.

  • New primary-store adapters (services/storage/primaryProtectedStoreAdapter.ts +
    primaryProtectedStoreAdapters.ts + ragVectorsProtectedStoreAdapter.ts): images,
    binder assets (Blob↔bytes), codex (3-shape dispatch), app-data/settings, snapshots
    (generic explicit-key/inline-keyPath engine), and a bespoke per-project RAG-vector
    adapter (aggregate ↔ individual-record duality).
  • New orchestrator (services/storage/encryptionMigrationOrchestrator.ts) combines
    the new primary adapters with the pre-existing secondary-store adapters
    (scene revisions, inference cache) into one journal-backed migration run, with a
    progress callback threaded through protectedStoreMigration.ts.
  • Real clearIdbPassphrase()/rotateIdbPassphrase() in storageEncryptionService.ts
    replace the stubs that always threw IdbEncryptionMigrationRequiredError — each now
    migrates + verifies every protected store before touching the passphrase sentinel, and
    leaves the durable journal in place (not a partial verifier change) on failure.
  • UI: PassphraseModal gains 'disable'/'rotate' modes with a live migration
    progress bar; PrivacySection gains Change-passphrase/Disable buttons.
  • Recovery UX: new EncryptionRecoveryModal + an App.tsx startup check — if a
    disable/rotate migration was interrupted (reload/crash) before reaching 'completed',
    it takes priority over the normal unlock flow and lets the user re-enter their
    passphrase(s) to resume via resumeEncryptionMigration(). A 'recovery-required'
    journal (the migration's own verification found an inconsistency) is surfaced as an
    honest stuck state, not auto-fixed.
  • Fixes a circular import (storageEncryptionService → orchestrator → adapters →
    storageEncryptionService) via a dynamic import at the two call sites, matching this
    repo's existing convention (listenerMiddleware.ts, aiApi.ts).

Stacked PR: i18n key additions (9 new settings.privacy.* keys × 19 locales +
bundle rebuild, 144 files) are in a follow-up PR based on this branch, kept separate so
CodeAnt's ~100-file review threshold still fires on both.

Test plan

  • pnpm run typecheck — clean
  • pnpm run lint — clean
  • Real end-to-end migration round-trip tests (image, codex 3-shape, binder-asset
    Blob↔bytes, RAG-vector aggregate↔individual) through rekey then disable
  • resumeEncryptionMigration coverage: correct resume, wrong source passphrase,
    missing target passphrase for rekey
  • New EncryptionRecoveryModal component tests (11)
  • PassphraseModal/PrivacySection/useSettingsView disable/rotate + progress-bar
    tests
  • 119+68 = 187 tests passing across all touched suites locally
  • CI: Quality Gate, Build, E2E, Storybook, Lighthouse (pending)

Closes#338 items 1 & 2 (disable/rotate production flow); items 3 & 4 already closed
by #339.

🤖 Generated with Claude Code

Summary by Sourcery

Wire production disable and passphrase rotation flows for IndexedDB at-rest encryption, including migration orchestration, UI, and recovery UX.

New Features:

  • Add primary and RAG-vector protected-store migration adapters and a production orchestrator that runs journal-backed migrations across all encrypted stores.
  • Enable real clearIdbPassphrase and rotateIdbPassphrase operations that fully migrate, verify, and commit disable/rekey flows with resumable journals and progress callbacks.
  • Introduce an EncryptionRecoveryModal and startup check that prioritizes resuming interrupted disable/rotate migrations over the normal unlock flow.

Enhancements:

  • Extend passphrase and settings UX with new disable/rotate modes, live migration progress, and success toasts integrated into the privacy settings view.
  • Expose migration progress from the protected-store migration engine to the UI via structured callbacks, and surface it in PassphraseModal and recovery flows.
  • Clarify and update IDB encryption documentation and ADR to reflect the completed lifecycle, migration, and recovery behavior, including remaining work and references.

Documentation:

  • Update IDB encryption docs, ADR 0018, and project guidance to describe the now-complete at-rest encryption lifecycle, production migration wiring, recovery UX, and outstanding follow-up work.

Tests:

  • Add comprehensive unit and component tests covering production migration round trips over real data, passphrase disable/rotate flows, recovery resume behavior, and updated privacy/settings UI interactions.

CodeAnt-AI Description

Enable production encryption disable, passphrase rotation, and migration recovery

What Changed

  • Users can disable at-rest encryption or change their passphrase from Settings → Privacy.
  • Disable and rotation migrate and verify protected project data before changing encryption credentials, preserving images, codex data, binder assets, snapshots, RAG vectors, and other protected records.
  • Migration progress is shown while the operation runs, with clear errors when it fails or the session is locked.
  • Interrupted migrations resume after users re-enter the required passphrase(s); the recovery flow takes priority over the normal unlock prompt after a reload or crash.
  • Inconsistent migrations are shown as a manual-recovery state instead of being silently retried or altered.
  • Added coverage for disable, rotation, progress reporting, recovery, data round trips, and failure handling; updated encryption documentation to reflect the completed lifecycle.

Impact

✅ Passphrase rotation without losing protected project data
✅ Safe encryption disable after full data verification
✅ Resumable recovery after interrupted migrations

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Added options to disable or rotate encryption from Privacy settings.
    • Added migration progress indicators and resumable recovery for interrupted encryption changes.
    • Encryption changes now protect data across supported storage areas.
    • Added localized messaging for encryption status, migration, and recovery states.
  • Bug Fixes

    • Improved handling of interrupted or failed migrations to prevent inconsistent storage states.
  • Documentation

    • Updated encryption lifecycle, migration, recovery, and limitations documentation.
  • Tests

    • Expanded coverage for encryption setup, rotation, disabling, progress, and recovery flows.

qnbsand others added 4 commits August 12, 2026 15:45
…ble/rotate (#338 items 1+2)
Replaces the clearIdbPassphrase/rotateIdbPassphrase stubs with real journal-backed
migrations: new primary-store adapters (images, binder assets, codex, app-data,
snapshots, RAG vectors) plug into the existing secondary-store adapters via a new
encryptionMigrationOrchestrator.ts, giving the previously test-only migration engine
its first production callers. Disable/rotate now migrate and verify every protected
store before touching the passphrase sentinel, and fail closed into the resumable
journal (not a partial verifier change) on error.
Breaks a circular import (storageEncryptionService -> orchestrator -> adapters ->
storageEncryptionService) via a dynamic import at the two call sites, matching this
codebase's existing dynamic-import convention for avoiding heavy/circular static
coupling (listenerMiddleware.ts, aiApi.ts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…st encryption (#338 items 1+2)
Extends PassphraseModal with 'disable'/'rotate' modes (live migration progress bar),
adds Disable/Change-passphrase buttons to PrivacySection, and wires both through
useSettingsView to the real clearIdbPassphrase/rotateIdbPassphrase implementations.
Adds EncryptionRecoveryModal + an App.tsx startup check: if a disable/rotate journal
was interrupted (reload/crash) before reaching 'completed', it takes priority over the
normal unlock flow and lets the user re-enter their passphrase(s) to safely resume —
CryptoKey material is never persisted, only re-derivable. A 'recovery-required' journal
(the migration's own verification found an inconsistency) is surfaced as a distinct,
honest stuck state rather than a fake auto-fix.
Refactors clearIdbPassphrase/rotateIdbPassphrase's post-migration commit steps and
key/verifier derivation into shared helpers so resumeEncryptionMigration() (used by
the recovery UX) reaches the exact same end state as a fresh-start migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ry UX (#338)
Real-data round-trip tests (image, codex 3-shape dispatch, binder asset Blob<->bytes,
RAG vectors aggregate<->individual duality) through rekey then disable — the prior
disable/rotate tests only proved migration completes over empty stores. Adds
resumeEncryptionMigration coverage (correct resume, wrong source passphrase, missing
target passphrase for rekey), new EncryptionRecoveryModal component tests, and
disable/rotate mode + progress-bar tests for PassphraseModal, PrivacySection's new
Change/Disable buttons, and useSettingsView's handlePassphraseConfirm branches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Updates CLAUDE.md § At-rest encryption and Known Technical Debt, docs/IDB-ENCRYPTION.md,
and ADR-0018's status line to reflect the production migration wiring implemented in
this branch — the orchestrator, primary-store adapters, protectedWriteAdmission cross-tab
gate, and EncryptionRecoveryModal. Remaining gap noted honestly: no E2E coverage yet for
the disable/rotate/recovery round trips (unit + component tests only so far).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@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.

@codeant-ai

codeant-aiBot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

StatusCommitStarted (UTC)Finished (UTC)
✅ Reviewed your PR302de39Aug 12, 2026 · 15:2315:26

@codeant-ai

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
worldscript-studioReadyReadyPreviewAug 13, 2026 2:43am

@sourcery-ai

sourcery-aiBot commented Aug 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Wires the previously test-only IDB encryption migration journal into production disable/rotate flows by adding primary-store adapters, a migration orchestrator with progress callbacks, real clear/rotate implementations with recovery, and corresponding settings/recovery UI, while updating docs and tests to reflect the complete lifecycle.

Sequence diagram for disable/rotate migration flow from Settings

sequenceDiagram
actor User
participant PrivacySection
participant PassphraseModal
participant storageEncryptionService
participant encryptionMigrationOrchestrator
participant protectedStoreMigration
User ->> PrivacySection: Click Disable/Rotate buttons
PrivacySection ->> PassphraseModal: setPassphraseModal('disable'|'rotate')
User ->> PassphraseModal: Enter passphrase(s), confirm
PassphraseModal ->> PrivacySection: onConfirm(current, next)
PrivacySection ->> storageEncryptionService: clearIdbPassphrase(onProgress) / rotateIdbPassphrase(oldPassphrase, newPassphrase, onProgress)
storageEncryptionService ->> encryptionMigrationOrchestrator: runProductionEncryptionMigration({operation, keys, targetVerifier?, onProgress})
encryptionMigrationOrchestrator ->> protectedStoreMigration: runProtectedStoreMigration(journal, adapters, keys, onProgress)
protectedStoreMigration -->> PassphraseModal: onProgress(progress)
PassphraseModal -->> User: Migration progress bar
protectedStoreMigration -->> encryptionMigrationOrchestrator: journal (phase 'committing')
encryptionMigrationOrchestrator -->> storageEncryptionService: journal
storageEncryptionService ->> storageEncryptionService: commitDisableMigration(journal) / commitRekeyMigration(journal, targetKey)
storageEncryptionService -->> PrivacySection: success
PrivacySection -->> User: Toast (disabled/changed), updated encryptionReady
Loading

Sequence diagram for interrupted migration recovery on startup

sequenceDiagram
actor User
participant App
participant EncryptionRecoveryModal
participant storageEncryptionService
participant encryptionMigrationOrchestrator
participant protectedStoreMigration
App ->> storageEncryptionService: readEncryptionMigrationJournal()
storageEncryptionService -->> App: journal (phase !== 'completed')
App ->> EncryptionRecoveryModal: journal prop
User ->> EncryptionRecoveryModal: Enter passphrase(s), Resume
EncryptionRecoveryModal ->> storageEncryptionService: resumeEncryptionMigration(journal, {sourcePassphrase, targetPassphrase?}, onProgress)
storageEncryptionService ->> storageEncryptionService: deriveAndVerifySourceKeyFromSentinel()
storageEncryptionService ->> storageEncryptionService: deriveAndVerifyTargetKeyFromVerifier() [rekey]
storageEncryptionService ->> encryptionMigrationOrchestrator: resumeProductionEncryptionMigration(journal, keys, onProgress)
encryptionMigrationOrchestrator ->> protectedStoreMigration: runProtectedStoreMigration(journal, adapters, keys, onProgress)
protectedStoreMigration -->> EncryptionRecoveryModal: onProgress(progress)
EncryptionRecoveryModal -->> User: Migration progress bar
protectedStoreMigration -->> encryptionMigrationOrchestrator: journal (phase 'committing')
encryptionMigrationOrchestrator -->> storageEncryptionService: journal
storageEncryptionService ->> storageEncryptionService: commitDisableMigration(journal) / commitRekeyMigration(journal, targetKey)
storageEncryptionService -->> EncryptionRecoveryModal: success
EncryptionRecoveryModal ->> App: onRecovered()
App ->> App: setEnableIdbAtRestEncryption(false) [disable]
App -->> User: Normal unlock flow resumes
Loading

File-Level Changes

ChangeDetailsFiles
Implement production disable and passphrase rotation flows using the encryption migration journal and new orchestrator, including recovery support for interrupted migrations.
  • Replace clearIdbPassphrase/rotateIdbPassphrase stubs with real implementations that run journal-backed migrations across all protected stores, enforce locked-session and recovery-required constraints, and commit verifier/sentinel updates only after successful migration/verification.
  • Add helpers to clear stored KDF salt, commit disable/rekey migrations, derive/verify source/target keys from the sentinel and target verifier, and resume encryption migrations using re-entered passphrases and a recovery-oriented orchestrator entry point.
  • Expose idbDecryptWithKey for explicit-key decryption and export the journal record key constant so adapters can skip journal/sentinel records.
services/storage/storageEncryptionService.ts
services/storage/encryptionMigrationJournal.ts
services/storage/idbPassphraseSentinel.ts
Introduce primary and RAG-vector protected-store adapters plus an orchestrator that combines all primary and secondary adapters into a single production migration run with progress reporting.
  • Add primaryProtectedStoreAdapter factory and primaryProtectedStoreAdapters registry to migrate whole-value primary stores (app data, snapshots, images, binder assets, codex) with explicit/inline key handling, reserved keys, batch processing, and idempotent replay-safe transforms + verification.
  • Add a bespoke ragVectorsProtectedStoreAdapter that migrates RAG vectors per project between individual-record and aggregate encrypted shapes, ensuring atomic per-project conversion during enable/rekey/disable.
  • Add encryptionMigrationOrchestrator to start and resume production migrations, generating operation IDs, preparing journals that enumerate all adapters, and delegating to runProtectedStoreMigration with progress callbacks.
services/storage/primaryProtectedStoreAdapter.ts
services/storage/primaryProtectedStoreAdapters.ts
services/storage/ragVectorsProtectedStoreAdapter.ts
services/storage/encryptionMigrationOrchestrator.ts
Extend the protected-store migration engine to support progress callbacks for UI, and integrate them through settings hooks and modals for disable/rotate and recovery UX.
  • Add ProtectedStoreMigrationProgress model and callback type to protectedStoreMigration, emitting per-store migration and verification progress as adapters advance checkpoints.
  • Wire progress into PassphraseModal and EncryptionRecoveryModal components, rendering migration progress bars with accessible ARIA attributes for disable/rotate and recovery flows.
  • Update useSettingsView and PrivacySection to support disable/rotate actions: invoking clearIdbPassphrase/rotateIdbPassphrase with progress callbacks, tracking migrationProgress in state, and driving PassphraseModal modes and success toasts.
services/storage/protectedStoreMigration.ts
components/settings/PassphraseModal.tsx
components/settings/EncryptionRecoveryModal.tsx
hooks/useSettingsView.ts
components/settings/PrivacySection.tsx
Add startup recovery UX that prioritizes resuming interrupted disable/rotate migrations over normal unlock and updates documentation and guidance to reflect the completed lifecycle.
  • Introduce EncryptionRecoveryModal and wire it into App.tsx so that a non-completed encryption migration journal triggers recovery modal display, blocks IdbUnlockModal, and on success updates the encryption feature flag for disable operations.
  • Update IDB-ENCRYPTION.md, ADR 0018, and CLAUDE.md to reflect completed disable/rekey/recovery implementation, describe recovery-required behavior, progress reporting, protected-write admission, and remaining E2E/duckdb work.
  • Expose the journal record key for tests and ensure mocks provide sentinel record key constants required by new adapters.
App.tsx
components/settings/EncryptionRecoveryModal.tsx
docs/IDB-ENCRYPTION.md
docs/adr/0018-idb-encryption-lifecycle-and-recovery.md
CLAUDE.md
tests/unit/storageEncryptionService.test.ts
Expand and adjust unit/component tests to cover production disable/rekey, recovery flows, migration round-trips against real data, and new settings UI behaviors.
  • Update storageEncryptionService unit tests to exercise real clearIdbPassphrase/rotateIdbPassphrase behavior, locked-session error handling, sentinel preservation, and correct verifier/sentinel changes plus journal recovery helpers.
  • Add storageEncryptionService tests that drive production migration adapters via dbService for images, codex, binder assets, and RAG vectors, asserting data survives rekey then disable and RAG vectors’ aggregate/individual duality is respected.
  • Extend PassphraseModal, PrivacySection, useSettingsView, EncryptionRecoveryModal, and storage tests to validate new modes (disable/rotate), progress handling, toasts, modal opening logic, and recovery behaviors including stuck recovery-required journals.
tests/unit/storage/storageEncryptionService.test.ts
tests/unit/storage/storageEncryptionService.test.ts
tests/unit/storage/storageEncryptionService.test.ts
tests/unit/settings/PassphraseModal.test.tsx
tests/unit/hooks/useSettingsView.test.ts
tests/unit/settings/PrivacySection.test.tsx
tests/unit/settings/EncryptionRecoveryModal.test.tsx

Assessment against linked issues

IssueObjectiveAddressedExplanation
#338Implement real production flows for disabling at-rest encryption and rotating the passphrase, including UI entry points and wiring these flows to the encryption migration journal (beginEncryptionMigration/runProtectedStoreMigration) with recovery UX.
#338Include secondary protected store adapters (e.g., sceneRevisionAdapterSpec, inferenceCacheAdapterSpec) in the production migration startup with checkpoint IDs matching their databaseName/storeName formats.
#338Update documentation, including CLAUDE.md Known Technical Debt B-1 and IDB-ENCRYPTION.md, to reflect that Phase 4 production migration wiring for IDB encryption is implemented.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codeant-aicodeant-aiBot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Aug 12, 2026
@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52b09bc3-09fe-4681-89bf-9d92947be7e3

📥 Commits

Reviewing files that changed from the base of the PR and between 4b43e9e and c5d700b.

📒 Files selected for processing (1)
  • README.md

📝 Walkthrough

Walkthrough

IndexedDB encryption now supports production disable and passphrase rotation across registered protected stores. Migrations persist journals, report progress, verify converted data, and resume after interruption through startup recovery UI.

Changes

Encryption migration engine

Layer / File(s)Summary
Protected-store migration adapters
services/storage/primaryProtectedStoreAdapter.ts, services/storage/primaryProtectedStoreAdapters.ts, services/storage/ragVectorsProtectedStoreAdapter.ts, services/storage/protectedStoreMigration.ts
Added batched, transactional, resumable, and verified migrations for primary stores and RAG vectors.
Migration orchestration and commit lifecycle
services/storage/encryptionMigrationOrchestrator.ts, services/storage/storageEncryptionService.ts, services/storage/encryptionMigrationJournal.ts, services/storage/idbPassphraseSentinel.ts
Added journal-backed production migrations, key verification, disable and rekey commits, progress callbacks, and recovery resumption.
Settings migration flows
hooks/useSettingsView.ts, components/settings/PrivacySection.tsx, components/settings/PassphraseModal.tsx
Added disable and rotate actions with mode-specific validation, progress display, success handling, and failure handling.
Startup recovery
App.tsx, components/settings/EncryptionRecoveryModal.tsx, components/ui/ErrorBoundary.tsx
Detects unfinished migrations during startup, blocks normal unlock, and provides resumable or recovery-required states with a custom fallback.
Validation, documentation, and localization
tests/unit/storage/*, tests/unit/hooks/useSettingsView.test.ts, tests/unit/settings/*, docs/*, CLAUDE.md, locales/*, public/locales/*
Added coverage for migration, recovery, progress, and protected-store data preservation. Updated lifecycle documentation and localized encryption messages.

Estimated code review effort: 5 (Critical) | ~120 minutes

Mergeability Score:🔵 Low · up to c5d70

The PR adds production encryption disable/rotation and recovery flows. It is mergeable with explicit owner follow-up for two bounded security-maintenance issues: an executable test file is broadly excluded from secret scanning, and a vulnerability ignore record is malformed or mismatched to its documented advisory.

Sequence Diagram(s)

sequenceDiagram
participant User
participant PrivacySection
participant useSettingsView
participant storageEncryptionService
participant encryptionMigrationOrchestrator
participant ProtectedStores
User->>PrivacySection: select disable or rotate
PrivacySection->>useSettingsView: submit passphrase operation
useSettingsView->>storageEncryptionService: start migration
storageEncryptionService->>encryptionMigrationOrchestrator: create journal and migrate
encryptionMigrationOrchestrator->>ProtectedStores: transform and verify records
ProtectedStores-->>useSettingsView: migration progress
encryptionMigrationOrchestrator-->>storageEncryptionService: completed journal
storageEncryptionService-->>useSettingsView: commit encryption state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR implements production disable/rotation and recovery UX [#338], but it does not show secondary-adapter wiring, shared write admission, or deletion-tolerant verification.Add and test scene-revision and inference-cache adapters, cross-tab write admission, and verification that tolerates legitimate concurrent deletions.
Out of Scope Changes check⚠️ WarningThe PR includes unrelated local-AI and writing-surface localization changes, README metric updates, and security-scan configuration changes outside encryption migration [#338].Remove unrelated localization, README, and scanner changes or move them to separate pull requests.
Docstring Coverage⚠️ WarningDocstring coverage is 43.24% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the production disable/rotate wiring and recovery UX, which are the main changes described in the pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/encryption-migration-phase4-b

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

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Both primaryProtectedStoreAdapter.ts and ragVectorsProtectedStoreAdapter.ts implement very similar openExistingDatabase/withDatabase logic; consider extracting a shared IndexedDB helper to reduce duplication and keep adapter code focused on migration semantics.
  • The progress fraction and progress bar rendering logic is duplicated between PassphraseModal and EncryptionRecoveryModal; a small shared utility for converting ProtectedStoreMigrationProgress to a normalized percentage would make the UX code easier to maintain and keep the behavior consistent.
  • ProtectedStoreMigrationProgress currently reports per-store index and processed counts, but the UI only uses storeIndex/storeCount and ignores processed; it may be worth clarifying or documenting the intended semantics (stores vs. records) so future consumers don’t misinterpret or overfit to the current usage.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- Both primaryProtectedStoreAdapter.ts and ragVectorsProtectedStoreAdapter.ts implement very similar openExistingDatabase/withDatabase logic; consider extracting a shared IndexedDB helper to reduce duplication and keep adapter code focused on migration semantics.
- The progress fraction and progress bar rendering logic is duplicated between PassphraseModal and EncryptionRecoveryModal; a small shared utility for converting ProtectedStoreMigrationProgress to a normalized percentage would make the UX code easier to maintain and keep the behavior consistent.
- ProtectedStoreMigrationProgress currently reports per-store index and processed counts, but the UI only uses storeIndex/storeCount and ignores processed; it may be worth clarifying or documenting the intended semantics (stores vs. records) so future consumers don’t misinterpret or overfit to the current usage.
## Individual Comments### Comment 1
<locationpath="tests/unit/hooks/useSettingsView.test.ts"line_range="605-614" />
<code_context>
+describe('handlePassphraseConfirm — disable/rotate', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add rotate-failure coverage to mirror the disable-failure behavior
Current tests cover disable success, rotate success, and disable failure, but not rotate failure (e.g. `mockRotateIdbPassphrase` rejecting). Since the expected behavior on rotate error is to keep the modal open, clear `migrationProgress`, and avoid a success toast, please add a test that mocks `rotateIdbPassphrase` rejecting and asserts:
-`migrationProgress` is cleared
-`passphraseModal` stays `'rotate'`-`mockToastSuccess` is not called.
This will bring rotate failure coverage in line with disable and verify the hook’s error handling more thoroughly.
Suggested implementation:
```typescriptdescribe('handlePassphraseConfirm — disable/rotate', () => {
afterEach(() => {
mockClearIdbPassphrase.mockResolvedValue(undefined);
mockRotateIdbPassphrase.mockResolvedValue(undefined);
});
it('clears migrationProgress, keeps passphraseModal as rotate, and does not toast on rotate failure', async () => {
const { result } =renderHook(() =>useSettingsView());
// Simulate a rotate errormockRotateIdbPassphrase.mockRejectedValue(newError('rotate failed'));
act(() => {
result.current.setPassphraseModal('rotate');
});
awaitact(async () => {
awaitresult.current.handlePassphraseConfirm();
});
// migrationProgress is cleared on errorexpect(result.current.migrationProgress).toBeNull();
// modal remains open in rotate stateexpect(result.current.passphraseModal).toBe('rotate');
// no success toast on errorexpect(mockToastSuccess).not.toHaveBeenCalled();
});
```To align this with the rest of the test file, you may need to:1. Adjust the `migrationProgress` assertion (`toBeNull()` vs `toBeUndefined()` or another expected cleared value) to match how the hook actually resets `migrationProgress` on error (mirror whatever the existing disable-failure test asserts).2. Ensure `mockRotateIdbPassphrase` and `mockToastSuccess` are already defined/mocked at the top of the file; if not, add appropriate `vi.fn()` mocks consistent with the existing conventions.3. If other tests use `waitFor` instead of `awaitact(async () =>...)` for async assertions on hook state, you can switch to `waitFor` to match the existing style.</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadtests/unit/hooks/useSettingsView.test.ts
@codeant-ai

codeant-aiBot commented Aug 12, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit:c5d700b5
Scan Time: 2026-08-13 03:25:06 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality GateStatusDetails
Secrets✅ PASSED0 secrets found
Duplicate Code✅ PASSED4.0% duplicated
SAST✅ PASSEDNo security issues
Bugs✅ PASSEDRating S: No bugs
IAC✅ PASSEDRating S: No issues

View Full Results

Comment threadApp.tsx
Comment threadcomponents/settings/EncryptionRecoveryModal.tsx
Comment threadcomponents/settings/EncryptionRecoveryModal.tsx Outdated
Comment threadservices/storage/primaryProtectedStoreAdapter.ts
Comment threadservices/storage/primaryProtectedStoreAdapters.ts
Comment threadservices/storage/storageEncryptionService.ts
Comment threadservices/storage/storageEncryptionService.ts
@gitguardian

gitguardianBot commented Aug 12, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian idGitGuardian statusSecretCommitFilename
36028823TriggeredGeneric Password0ddcb44tests/unit/storage/storageEncryptionService.test.tsView secret
36028823TriggeredGeneric Password423c115tests/unit/storage/storageEncryptionService.test.tsView secret
36042393TriggeredGeneric Password423c115tests/unit/storage/storageEncryptionService.test.tsView secret
36028823TriggeredGeneric Password423c115tests/unit/storage/storageEncryptionService.test.tsView secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
App.tsx (1)

354-379: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the journal read a startup barrier.

recoveryJournal starts as null, so the unlock effect can open IdbUnlockModal before readEncryptionMigrationJournal() resolves. This breaks the recovery-first contract on slow IndexedDB reads.

The async journal read also has no catch. A rejected read creates an unhandled rejection and leaves no safe recovery state.

Track checking, none, and journal states. Do not run normal unlock until the check completes. If the read fails, keep recovery blocked, show an actionable recovery error, and log sanitized diagnostics through services/logger.ts.

As per coding guidelines, async operations must use try/catch, and technical details must use services/logger.ts.

🤖 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.tsx` around lines 354 - 379, Make the migration-journal read in the
startup useEffect an explicit checking/none/journal state instead of using null
as the initial value, and gate the normal unlock effect until the check reaches
none. Wrap readEncryptionMigrationJournal in try/catch; on failure keep unlock
blocked, expose an actionable recovery error state, and log only sanitized
diagnostics through services/logger.ts. Update the recoveryJournal checks and
effect dependencies to preserve recovery-first behavior.

Source: Coding guidelines

🧹 Nitpick comments (12)
components/settings/PassphraseModal.tsx (1)

78-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep the QNBS-v3 rationale on one physical line.

Lines 78-80 wrap the added QNBS-v3 comment. Replace it with one physical line.

As per coding guidelines, “For every non-trivial code change, add one single-line QNBS-v3 comment explaining why … never wrap the comment across physical lines.”

🤖 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 `@components/settings/PassphraseModal.tsx` around lines 78 - 80, Update the
QNBS-v3 rationale comment in PassphraseModal so the entire explanation is on one
physical line, preserving its current meaning and wording without changing
surrounding logic.

Source: Coding guidelines

components/settings/PrivacySection.tsx (1)

116-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep the QNBS-v3 rationale on one physical line.

Lines 116-117 split the added QNBS-v3 comment across two lines. Condense this rationale into one physical comment line.

As per coding guidelines, “For every non-trivial code change, add one single-line QNBS-v3 comment explaining why … never wrap the comment across physical lines.”

🤖 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 `@components/settings/PrivacySection.tsx` around lines 116 - 117, Condense the
QNBS-v3 rationale comment near clearIdbPassphrase/rotateIdbPassphrase onto one
physical line, preserving its explanation that both require an unlocked session
key and re-migrate protected stores before touching the sentinel.

Source: Coding guidelines

tests/unit/settings/PrivacySection.test.tsx (1)

163-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a one-line QNBS-v3 rationale for the new action-flow tests.

These tests add rotate and disable interaction coverage. Add one physical-line QNBS-v3 comment that states why this regression coverage is required.

As per coding guidelines, “For every non-trivial code change, add one single-line QNBS-v3 comment explaining why … never wrap the comment across physical lines.”

🤖 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 `@tests/unit/settings/PrivacySection.test.tsx` around lines 163 - 188, Add one
physical-line QNBS-v3 comment near the rotate and disable action-flow tests
explaining that the regression coverage verifies the correct passphrase modal
opens for each encryption action. Keep the comment single-line and avoid adding
additional comments or wrapping it across lines.

Source: Coding guidelines

tests/unit/settings/PassphraseModal.test.tsx (1)

228-413: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a one-line QNBS-v3 rationale for the new modal-mode tests.

These suites add disable and rotate migration coverage. Add one physical-line QNBS-v3 comment that states why this coverage exists.

As per coding guidelines, “For every non-trivial code change, add one single-line QNBS-v3 comment explaining why … never wrap the comment across physical lines.”

🤖 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 `@tests/unit/settings/PassphraseModal.test.tsx` around lines 228 - 413, Add one
single-line QNBS-v3 comment adjacent to the new PassphraseModal disable and
rotate test suites, stating that the coverage verifies migration behavior and
mode-specific passphrase flows. Keep the rationale on exactly one physical line
and do not add additional comments.

Source: Coding guidelines

services/storage/encryptionMigrationOrchestrator.ts (1)

27-32: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert unique adapter ids when combining registries.

The journal maps checkpoints by adapter.id. If a primary adapter and a secondary adapter ever share an id, two stores share one checkpoint. The migration then reports progress and verification for the wrong store and can mark an unmigrated store as done. Fail fast on a duplicate id instead.

♻️ Suggested guard
 function getRegisteredProtectedStoreAdapters(): readonly ProtectedStoreAdapter[] {
- return [+ const adapters = [
...getRegisteredPrimaryProtectedStoreAdapters(),
...getRegisteredSecondaryProtectedStoreAdapters(),
];
+ // QNBS-v3: journal checkpoints are keyed by adapter id — a collision would silently share one checkpoint between two stores.+ const ids = new Set(adapters.map((adapter) => adapter.id));+ if (ids.size !== adapters.length) {+ throw new Error('Duplicate protected-store adapter id in the migration registry');+ }+ return adapters;
}
🤖 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 `@services/storage/encryptionMigrationOrchestrator.ts` around lines 27 - 32,
Update getRegisteredProtectedStoreAdapters to validate that every combined
ProtectedStoreAdapter has a unique id across the primary and secondary
registries, throwing immediately when a duplicate is detected before returning
the array.
services/storage/ragVectorsProtectedStoreAdapter.ts (1)

57-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

openExistingDatabase is duplicated.

This function is a verbatim copy of openExistingDatabase in services/storage/primaryProtectedStoreAdapter.ts (Lines 62-97). Its abort-on-create semantics are subtle, so two copies will diverge. Extract it into one shared storage helper and import it in both adapters.

As per coding guidelines: "Apply DRY: place reusable logic in services, hooks, or feature thunks instead of duplicating it in views."

🤖 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 `@services/storage/ragVectorsProtectedStoreAdapter.ts` around lines 57 - 104,
The openExistingDatabase logic is duplicated between
ragVectorsProtectedStoreAdapter and primaryProtectedStoreAdapter. Extract the
shared openExistingDatabase implementation into a reusable storage helper, then
import and use that helper from both adapters while preserving its
abort-on-create and existing error-handling behavior.

Source: Coding guidelines

services/storage/storageEncryptionService.ts (2)

736-737: 🔒 Security & Privacy | 🔵 Trivial

Passphrase rotation keeps the existing KDF salt.

getExistingSalt() derives the new key with the old salt. This is required by the resume design, because the journal stores only a target verifier and the key must stay re-derivable. It also means a rotation does not change the KDF input material. Record this trade-off in docs/IDB-ENCRYPTION.md or the ADR, so a future change to re-salt on rotation also plans journal-side salt versioning.

🤖 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 `@services/storage/storageEncryptionService.ts` around lines 736 - 737,
Document in docs/IDB-ENCRYPTION.md or the relevant ADR that passphrase rotation
in the flow using _svc.deriveKey and getExistingSalt intentionally reuses the
existing KDF salt so the target key remains re-derivable during resume from the
journal. Record that future re-salting must include journal-side salt
versioning.

662-663: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

This file now exceeds the 700-line target.

The new commit, derive, and resume helpers push storageEncryptionService.ts past 817 lines. Consider moving the migration lifecycle helpers, commitDisableMigration, commitRekeyMigration, deriveAndVerifySourceKeyFromSentinel, deriveAndVerifyTargetKeyFromVerifier, and resumeEncryptionMigration, into a dedicated module. The crypto primitives and the session-key state then stay separate from the lifecycle orchestration.

As per coding guidelines: "Target files between 200 and 700 lines; split files over 700 lines into hooks, subcomponents, selectors, or tests rather than using comment-only sections."

🤖 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 `@services/storage/storageEncryptionService.ts` around lines 662 - 663, Extract
the migration lifecycle orchestration from storageEncryptionService.ts into a
dedicated module, including the migration lifecycle helpers,
commitDisableMigration, commitRekeyMigration,
deriveAndVerifySourceKeyFromSentinel, deriveAndVerifyTargetKeyFromVerifier, and
resumeEncryptionMigration. Update imports and call sites so crypto primitives
and session-key state remain in storageEncryptionService.ts while all existing
migration behavior is preserved.

Source: Coding guidelines

services/storage/protectedStoreMigration.ts (1)

271-277: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Isolate progress-callback failures from the migration run.

onProgress is supplied by UI code. If a callback throws, the exception propagates out of the migrating loop, aborts the run, and releases the lease, even though every store write and journal checkpoint succeeded. Wrap the invocation so reporting cannot fail a crypto migration.

♻️ Suggested guard (apply to both call sites)
+// QNBS-v3: progress reporting is observability only — a throwing UI callback must never abort a durable migration.+function reportProgress(+ onProgress: ProtectedStoreMigrationProgressCallback | undefined,+ progress: ProtectedStoreMigrationProgress,+): void {+ if (!onProgress) return;+ try {+ onProgress(progress);+ } catch {+ /* ignore reporting failures */+ }+}
- onProgress?.({- storeId: adapter.id,- storeIndex,- storeCount: adapters.length,- phase: 'migrating',- processed: checkpoint.processed,- });+ reportProgress(onProgress, {+ storeId: adapter.id,+ storeIndex,+ storeCount: adapters.length,+ phase: 'migrating',+ processed: checkpoint.processed,+ });
🤖 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 `@services/storage/protectedStoreMigration.ts` around lines 271 - 277, Wrap
both onProgress invocations in the migration flow, including the call near the
migrating phase, with error isolation so exceptions from UI callbacks are caught
and do not propagate into the store loop or abort the migration. Keep progress
reporting best-effort while preserving the existing migration, checkpoint, and
lease behavior.
services/storage/primaryProtectedStoreAdapters.ts (1)

77-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The rekey fallback discards the real failure cause in every adapter. Each adapter probes the target key first, then falls back to the source key inside a nested try. The inner catch rethrows the target-probe error and drops the source-side error, so a failed rotation always reports the wrong cause.

  • services/storage/primaryProtectedStoreAdapters.ts#L77-L88: keep the source-decrypt error as cause on the thrown ProtectedStoreMigrationAdapterError; apply the same change at Lines 270-285 and Lines 400-410.
  • services/storage/ragVectorsProtectedStoreAdapter.ts#L181-L207: move requireKey(context.sourceKey, 'source') out of the inner try and attach the source-decrypt error as cause.
🤖 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 `@services/storage/primaryProtectedStoreAdapters.ts` around lines 77 - 88,
Update the rekey fallback error handling in
services/storage/primaryProtectedStoreAdapters.ts at lines 77-88, 270-285, and
400-410 so the source-key decryption failure is thrown as
ProtectedStoreMigrationAdapterError with that error attached as cause, rather
than rethrowing the target-probe error. In
services/storage/ragVectorsProtectedStoreAdapter.ts at lines 181-207, move
requireKey(context.sourceKey, 'source') outside the inner try and attach the
source-decrypt error as cause; apply this consistently across all affected
adapters.
tests/unit/storage/storageEncryptionService.test.ts (1)

746-779: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the nondeterministic operation ID and the duplicated salt key.

Line 768 builds operationId from Math.random(). Line 748 hardcodes the KDF salt storage key 'worldscript-idb-kdf-salt-v1'. A counter keeps the ID deterministic, and importing the production constant keeps the helper aligned if the key changes.

As per coding guidelines: "Make tests deterministic by mocking Date.now(), using fake timers, resetting global state in beforeEach, and never relying on network or test order."

♻️ Proposed deterministic operation ID
+ // QNBS-v3: deterministic operationId — a counter keeps journal IDs reproducible across runs.+ let recoveryJournalSeq = 0;+
async function buildPendingRekeyJournal(targetPassphrase: string) {
@@
return beginEncryptionMigration({
- operationId: `recovery-test-${Math.random().toString(36).slice(2)}`,+ operationId: `recovery-test-${++recoveryJournalSeq}`,
🤖 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 `@tests/unit/storage/storageEncryptionService.test.ts` around lines 746 - 779,
Update readCurrentSaltForTest to use the production KDF salt storage-key
constant instead of the hardcoded string, and replace the Math.random()-based
operationId in buildPendingRekeyJournal with a deterministic counter that is
reset between tests.

Source: Coding guidelines

tests/unit/storageEncryptionService.test.ts (1)

24-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the reserved record key from the real module.

Line 26 duplicates the production value of PASSPHRASE_SENTINEL_RECORD_KEY as a string literal. If the production constant changes, this mock keeps the old value and primaryProtectedStoreAdapters.ts receives a stale reserved key while the suite stays green. Re-export the real constant inside the mock factory instead.

♻️ Proposed fix using the actual module value
- // QNBS-v3: primaryProtectedStoreAdapters.ts reads this reserved-key constant at module load —- // the mock factory must supply it too, not just the three sentinel CRUD functions.- PASSPHRASE_SENTINEL_RECORD_KEY: 'idb_passphrase_sentinel_v1',+ // QNBS-v3: primaryProtectedStoreAdapters.ts reads this reserved-key constant at module load —+ // the mock factory must supply it too, and it must track the real value to avoid silent drift.+ PASSPHRASE_SENTINEL_RECORD_KEY: (+ await vi.importActual<typeof import('../../services/storage/idbPassphraseSentinel')>(+ '../../services/storage/idbPassphraseSentinel',+ )+ ).PASSPHRASE_SENTINEL_RECORD_KEY,

Adjust the module path to the real sentinel module path, and make the mock factory async.

🤖 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 `@tests/unit/storageEncryptionService.test.ts` around lines 24 - 27, Update the
mock factory in storage encryption tests to derive
PASSPHRASE_SENTINEL_RECORD_KEY from the real sentinel module instead of
duplicating the string literal. Use the correct production module path and make
the mock factory async so it can import and re-export the actual constant while
preserving the existing mocked CRUD functions.
🤖 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.tsx`:
- Around line 206-207: Convert the QNBS-v3 annotation at App.tsx lines 206-207
into one physical line; make the same change at App.tsx lines 354-356. Add one
physical-line QNBS-v3 comment for the recovery behavior in
components/settings/EncryptionRecoveryModal.tsx lines 17-24, and replace the
wrapped annotation in tests/unit/settings/EncryptionRecoveryModal.test.tsx lines
1-5 with one physical line. Each non-trivial change must retain a single-line
QNBS-v3 comment without wrapping.
- Around line 829-840: Update the recovery UI around EncryptionRecoveryModal in
App so an ErrorBoundary failure still renders a recovery-specific blocking
fallback with a backdrop and focus trap, or move the recovery guard outside the
normal application tree. Preserve the recovery lock for as long as
recoveryJournal is set and keep the existing onRecovered cleanup behavior
unchanged.
In `@components/settings/EncryptionRecoveryModal.tsx`:
- Line 120: Update both password input className declarations in
components/settings/EncryptionRecoveryModal.tsx at lines 120-120 and 144-144,
replacing the --sc-border-focus ring token with --sc-ring-focus while preserving
the required focus-visible:ring-2 classes.
- Around line 52-70: Update handleResume in EncryptionRecoveryModal to
distinguish credential-related failures from invalid journal, missing verifier,
and migration errors; retain encryptionWrongPassphrase only for credential
failures and show a separate actionable recovery-failure message otherwise. Log
sanitized technical context for non-credential failures through
services/logger.ts, and add a test covering a non-credential rejection.
In `@components/settings/PassphraseModal.tsx`:
- Line 14: Update the contract comment for onConfirm in PassphraseModal to state
that only unlock receives (passphrase, ''), while disable receives ('', '') and
the existing set and rotate contracts remain accurate.
- Around line 77-87: Update the catch handling in PassphraseModal’s
rotateIdbPassphrase flow so only typed old-passphrase verification failures use
encryptionWrongPassphrase; map journal, recovery-required, and migration
failures to brief actionable migration/recovery messages, and log sanitized
diagnostics through services/logger.ts. In
tests/unit/settings/PassphraseModal.test.tsx lines 392-412, retain the typed
old-passphrase assertion and add coverage for migration and recovery failures
with the new user-facing errors.
- Around line 196-217: Add aria-live="polite" to the migration progress status
text or its containing element in PassphraseModal’s busy progress block,
ensuring dynamic updates to the translated current/total progress are announced
without changing the existing progressbar behavior.
- Around line 129-130: Update the input className declarations in
PassphraseModal so their focus-visible ring utility uses the required
--sc-ring-focus token instead of --sc-border-focus, preserving the existing
focus-visible:ring-2 styling across all referenced inputs.
- Around line 104-105: Update the Modal invocation in PassphraseModal to pass
isDismissible={!busy}, keeping backdrop and Escape dismissal disabled while
clearIdbPassphrase or rotateIdbPassphrase is active and restoring dismissal when
the migration completes.
In `@docs/adr/0018-idb-encryption-lifecycle-and-recovery.md`:
- Line 3: Update the contradictory lifecycle statements in the ADR sections
around the pending conversion policy, remaining work, and UI availability to
reflect that disable and rekey operations are shipped. Preserve the documented
RECOVERY_REQUIRED manual-recovery limitation, and identify remaining work such
as E2E coverage and manual recovery instead of obsolete lease or
lifecycle-enablement prerequisites.
In `@docs/IDB-ENCRYPTION.md`:
- Around line 145-158: The documentation incorrectly states that protected store
writers call assertIdbProtectedWriteAllowed() through
withProtectedWriteAdmission(). Update the protected-store writer call-path
description to reflect withProtectedWriteAdmission() →
resolveProtectedWriteKey() → assertNoActiveEncryptionMigration() before
transactions, while noting that delete paths invoke
assertIdbProtectedWriteAllowed() directly and commitDisableMigration removes the
KDF salt via clearStoredSalt() after deleting the sentinel.
In `@services/storage/primaryProtectedStoreAdapter.ts`:
- Around line 294-313: Make clearGeminiApiKey() and clearApiKey() acquire
withProtectedWriteAdmission before deleting APP_DATA_STORE records, matching
migration write coordination. Update verify() and its migration accounting to
tolerate records deleted after processing by using stable migration-scope or
deletion-aware counts, so valid deletions do not produce verified <
checkpoint.processed or recovery-required.
In `@services/storage/ragVectorsProtectedStoreAdapter.ts`:
- Around line 127-134: Update readProjectRecords to handle transaction.onabort
by rejecting the promise with the transaction’s error, while preserving the
existing request success and error handlers so the promise always settles during
transaction aborts.
In `@services/storage/storageEncryptionService.ts`:
- Around line 664-671: Update resumeEncryptionMigration and the commit helpers
in services/storage/storageEncryptionService.ts at lines 664-671 and 673-683:
for a disable journal in committing state with a missing sentinel, treat
migration as already applied and complete journal bookkeeping without deriving a
key; for a rekey journal in committing state, validate the entered passphrase
against the current stored sentinel, including when it is already the new
sentinel, before reporting an incorrect passphrase.
In `@tests/unit/settings/EncryptionRecoveryModal.test.tsx`:
- Around line 34-50: Update the Modal mock in EncryptionRecoveryModal tests to
accept and expose isDismissible, then assert it is false on the rendered
recovery dialog. Preserve the existing isOpen behavior while ensuring tests fail
if the recovery modal becomes dismissible.
In `@tests/unit/storage/storageEncryptionService.test.ts`:
- Around line 627-655: Update the migration test setup, especially the shared
beforeEach used by clearIdbPassphrase and rotateIdbPassphrase tests, to reset
all relevant IndexedDB databases and cached connections in addition to the
sentinel and local storage. Ensure images, codex records, binder assets, and
vectors are removed between tests so later migrations start with isolated state.
---
Outside diff comments:
In `@App.tsx`:
- Around line 354-379: Make the migration-journal read in the startup useEffect
an explicit checking/none/journal state instead of using null as the initial
value, and gate the normal unlock effect until the check reaches none. Wrap
readEncryptionMigrationJournal in try/catch; on failure keep unlock blocked,
expose an actionable recovery error state, and log only sanitized diagnostics
through services/logger.ts. Update the recoveryJournal checks and effect
dependencies to preserve recovery-first behavior.
---
Nitpick comments:
In `@components/settings/PassphraseModal.tsx`:
- Around line 78-80: Update the QNBS-v3 rationale comment in PassphraseModal so
the entire explanation is on one physical line, preserving its current meaning
and wording without changing surrounding logic.
In `@components/settings/PrivacySection.tsx`:
- Around line 116-117: Condense the QNBS-v3 rationale comment near
clearIdbPassphrase/rotateIdbPassphrase onto one physical line, preserving its
explanation that both require an unlocked session key and re-migrate protected
stores before touching the sentinel.
In `@services/storage/encryptionMigrationOrchestrator.ts`:
- Around line 27-32: Update getRegisteredProtectedStoreAdapters to validate that
every combined ProtectedStoreAdapter has a unique id across the primary and
secondary registries, throwing immediately when a duplicate is detected before
returning the array.
In `@services/storage/primaryProtectedStoreAdapters.ts`:
- Around line 77-88: Update the rekey fallback error handling in
services/storage/primaryProtectedStoreAdapters.ts at lines 77-88, 270-285, and
400-410 so the source-key decryption failure is thrown as
ProtectedStoreMigrationAdapterError with that error attached as cause, rather
than rethrowing the target-probe error. In
services/storage/ragVectorsProtectedStoreAdapter.ts at lines 181-207, move
requireKey(context.sourceKey, 'source') outside the inner try and attach the
source-decrypt error as cause; apply this consistently across all affected
adapters.
In `@services/storage/protectedStoreMigration.ts`:
- Around line 271-277: Wrap both onProgress invocations in the migration flow,
including the call near the migrating phase, with error isolation so exceptions
from UI callbacks are caught and do not propagate into the store loop or abort
the migration. Keep progress reporting best-effort while preserving the existing
migration, checkpoint, and lease behavior.
In `@services/storage/ragVectorsProtectedStoreAdapter.ts`:
- Around line 57-104: The openExistingDatabase logic is duplicated between
ragVectorsProtectedStoreAdapter and primaryProtectedStoreAdapter. Extract the
shared openExistingDatabase implementation into a reusable storage helper, then
import and use that helper from both adapters while preserving its
abort-on-create and existing error-handling behavior.
In `@services/storage/storageEncryptionService.ts`:
- Around line 736-737: Document in docs/IDB-ENCRYPTION.md or the relevant ADR
that passphrase rotation in the flow using _svc.deriveKey and getExistingSalt
intentionally reuses the existing KDF salt so the target key remains
re-derivable during resume from the journal. Record that future re-salting must
include journal-side salt versioning.
- Around line 662-663: Extract the migration lifecycle orchestration from
storageEncryptionService.ts into a dedicated module, including the migration
lifecycle helpers, commitDisableMigration, commitRekeyMigration,
deriveAndVerifySourceKeyFromSentinel, deriveAndVerifyTargetKeyFromVerifier, and
resumeEncryptionMigration. Update imports and call sites so crypto primitives
and session-key state remain in storageEncryptionService.ts while all existing
migration behavior is preserved.
In `@tests/unit/settings/PassphraseModal.test.tsx`:
- Around line 228-413: Add one single-line QNBS-v3 comment adjacent to the new
PassphraseModal disable and rotate test suites, stating that the coverage
verifies migration behavior and mode-specific passphrase flows. Keep the
rationale on exactly one physical line and do not add additional comments.
In `@tests/unit/settings/PrivacySection.test.tsx`:
- Around line 163-188: Add one physical-line QNBS-v3 comment near the rotate and
disable action-flow tests explaining that the regression coverage verifies the
correct passphrase modal opens for each encryption action. Keep the comment
single-line and avoid adding additional comments or wrapping it across lines.
In `@tests/unit/storage/storageEncryptionService.test.ts`:
- Around line 746-779: Update readCurrentSaltForTest to use the production KDF
salt storage-key constant instead of the hardcoded string, and replace the
Math.random()-based operationId in buildPendingRekeyJournal with a deterministic
counter that is reset between tests.
In `@tests/unit/storageEncryptionService.test.ts`:
- Around line 24-27: Update the mock factory in storage encryption tests to
derive PASSPHRASE_SENTINEL_RECORD_KEY from the real sentinel module instead of
duplicating the string literal. Use the correct production module path and make
the mock factory async so it can import and re-export the actual constant while
preserving the existing mocked CRUD functions.
🪄 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

Run ID: db5da394-ac82-4b4c-a6e1-6a55cd8a194e

📥 Commits

Reviewing files that changed from the base of the PR and between dce75b3 and 302de39.

📒 Files selected for processing (22)
  • App.tsx
  • CLAUDE.md
  • components/settings/EncryptionRecoveryModal.tsx
  • components/settings/PassphraseModal.tsx
  • components/settings/PrivacySection.tsx
  • docs/IDB-ENCRYPTION.md
  • docs/adr/0018-idb-encryption-lifecycle-and-recovery.md
  • hooks/useSettingsView.ts
  • services/storage/encryptionMigrationJournal.ts
  • services/storage/encryptionMigrationOrchestrator.ts
  • services/storage/idbPassphraseSentinel.ts
  • services/storage/primaryProtectedStoreAdapter.ts
  • services/storage/primaryProtectedStoreAdapters.ts
  • services/storage/protectedStoreMigration.ts
  • services/storage/ragVectorsProtectedStoreAdapter.ts
  • services/storage/storageEncryptionService.ts
  • tests/unit/hooks/useSettingsView.test.ts
  • tests/unit/settings/EncryptionRecoveryModal.test.tsx
  • tests/unit/settings/PassphraseModal.test.tsx
  • tests/unit/settings/PrivacySection.test.tsx
  • tests/unit/storage/storageEncryptionService.test.ts
  • tests/unit/storageEncryptionService.test.ts

Comment threadApp.tsx Outdated
Comment threadApp.tsx
Comment threadcomponents/settings/EncryptionRecoveryModal.tsx
Comment threadcomponents/settings/EncryptionRecoveryModal.tsx Outdated
Comment threadcomponents/settings/PassphraseModal.tsx Outdated
Comment threadservices/storage/primaryProtectedStoreAdapter.ts
Comment threadservices/storage/ragVectorsProtectedStoreAdapter.ts
Comment threadservices/storage/storageEncryptionService.ts
Comment threadtests/unit/settings/EncryptionRecoveryModal.test.tsx
Comment threadtests/unit/storage/storageEncryptionService.test.ts
@qodo-code-review

qodo-code-reviewBot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0)📘 Rule violations (2)📜 Skill insights (0)

Grey Divider


Action required

1. Rekey loses source verifier✓ Resolved🐞 Bug☼ Reliability
Description
commitRekeyMigration replaces the old sentinel before completing the journal, so a crash or
journal-write failure leaves a committing migration whose current sentinel authenticates only the
new passphrase. Recovery nevertheless verifies the old passphrase against that replaced sentinel
first, causing the correct old/new pair to fail permanently.
Code

services/storage/storageEncryptionService.ts[R678-679]

+ await savePassphraseSentinel(newSentinel.bytes);+ await completeEncryptionMigration(journal);
Relevance

●● Moderate

Rekey crash-consistency/sentinel ordering concern is complex; lacks close accepted/rejected
precedent in repo history.

PR-#339

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new sentinel is persisted before the journal transition. Resume always derives the source key
against the current sentinel before checking the journal phase or target verifier, while the
recovery modal requires both passphrases for every rekey journal.

services/storage/storageEncryptionService.ts[673-682]
services/storage/storageEncryptionService.ts[757-777]
services/storage/storageEncryptionService.ts[786-816]
components/settings/EncryptionRecoveryModal.tsx[37-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Rekey makes the target sentinel authoritative before the journal is durably completed, but recovery still depends on authenticating the source passphrase against the current sentinel.
## Issue Context
Keep an authenticated source verifier until completion, atomically update sentinel and journal in their shared object store, or special-case `committing` recovery so it can finalize using the journal target verifier without re-deriving an unnecessary source key.
## Fix Focus Areas
- services/storage/storageEncryptionService.ts[673-682]
- services/storage/storageEncryptionService.ts[786-816]
- services/storage/encryptionMigrationJournal.ts[456-466]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Disable loses recovery key✓ Resolved🐞 Bug☼ Reliability
Description
commitDisableMigration deletes the sentinel and salt before durably completing the journal, so a
crash or completion-write failure leaves a non-completed journal with no credentials from which
recovery can derive the source key. Startup then forces the recovery modal, but every resume attempt
fails before it can finish the already-verified commit.
Code

services/storage/storageEncryptionService.ts[R665-667]

+ await deletePassphraseSentinel();+ clearStoredSalt();+ await completeEncryptionMigration(journal);
Relevance

●● Moderate

Crash-consistency ordering issue is plausible but invasive; no close precedent on journal commit
ordering changes.

PR-#339

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The commit sequence deletes the sentinel and salt first, but resume unconditionally authenticates
the source passphrase against that sentinel using the existing salt. App startup treats the
still-committing journal as mandatory recovery, making this intermediate state user-visible but
impossible to resume.

services/storage/storageEncryptionService.ts[664-670]
services/storage/storageEncryptionService.ts[757-763]
services/storage/storageEncryptionService.ts[786-799]
App.tsx[354-360]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
Disable removes all source-key recovery material before the durable journal reaches `completed`, creating a non-resumable crash window.
## Issue Context
Commit the sentinel deletion and journal transition atomically where possible, and do not remove the salt until the durable state can be recovered. Alternatively, make a `committing` disable journal finish metadata cleanup without requiring the source passphrase.
## Fix Focus Areas
- services/storage/storageEncryptionService.ts[664-670]
- services/storage/storageEncryptionService.ts[786-799]
- services/storage/encryptionMigrationJournal.ts[456-466]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. EncryptionRecoveryModal cannot be dismissed✗ Dismissed📘 Rule violation☑ Accessibility
Description
EncryptionRecoveryModal is rendered non-dismissible (isDismissible={false} with a no-op
onClose), preventing ESC/backdrop close and violating modal accessibility requirements. Because
App.tsx prioritizes showing this modal whenever a non-completed encryption migration journal
exists, users who cannot provide the correct passphrase have no in-app escape/reset path and can be
effectively locked out on every reload until manually deleting the journal from IndexedDB.
Code

components/settings/EncryptionRecoveryModal.tsx[R79-82]

+ <Modal+ isOpen={true}+ onClose={() => undefined}+ isDismissible={false}
Relevance

●● Moderate

Accessibility precedent exists, but making recovery modal dismissible is product/security-sensitive
and may be contested.

PR-#138
PR-#125

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2527276 requires modals to trap focus and be dismissible via ESC/backdrop, but
components/settings/EncryptionRecoveryModal.tsx explicitly sets isDismissible={false} and
onClose={() => undefined}, disabling those dismissal mechanisms and offering no cancel/skip
control. In App.tsx, startup logic reads the encryption migration journal and, when its phase is
not completed, sets recoveryJournal and renders `{recoveryJournal && (<EncryptionRecoveryModal
... />)} ahead of the normal unlock/app flow; the journal is only cleared via onRecovered` after a
successful resumeEncryptionMigration() path, so a user who cannot re-derive the required
passphrase(s) is blocked from the application UI on subsequent loads with no documented in-app
recovery/reset option besides direct IndexedDB manipulation.

Rule 2527276: Modals must implement focus trap and ESC to close
components/settings/EncryptionRecoveryModal.tsx[79-83]
components/settings/EncryptionRecoveryModal.tsx[78-84]
App.tsx[826-841]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`EncryptionRecoveryModal` is currently implemented as non-dismissible (`isDismissible={false}` and a no-op `onClose`), which prevents ESC/backdrop closing and violates the modal accessibility requirement to support focus trapping and ESC-to-close behavior. Additionally, `App.tsx` renders this modal with priority whenever a non-completed encryption migration journal is detected at startup, so if a user cannot supply the correct passphrase(s) to resume the migration there is no in-app way to abandon/reset the flow and regain access, leaving the app unusable across reloads until the journal is manually deleted from IndexedDB.
## Issue Context
- Compliance (PR Compliance ID 2527276) requires modals to trap focus and be closable via ESC/backdrop (including a closeable backdrop button).
- `EncryptionRecoveryModal.tsx` disables dismissal via `isDismissible={false}` and `onClose={() => undefined}`, and provides no cancel/skip/abandon control; the only “exit” is a successful `resumeEncryptionMigration()`.
- `App.tsx` startup reads the encryption migration journal and, if its phase isn’t `completed`, sets `recoveryJournal`, which causes `EncryptionRecoveryModal` to render ahead of the normal unlock/app UI; `recoveryJournal` is only cleared on successful recovery (`onRecovered`).
- While the documented `recovery-required` stuck state may be an intentional dead end, the resumable (non-stuck) case currently has the same lack of an escape route for users who simply cannot recall the required passphrase(s).
## Fix Focus Areas
- components/settings/EncryptionRecoveryModal.tsx[78-96]
- App.tsx[826-841]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. App metadata gets corrupted✓ Resolved🐞 Bug≡ Correctness
Description
The app-data adapter transforms every record except two lifecycle keys, so rotation also
JSON-encrypts the structured-cloned local_crypto_key_v2 CryptoKey used for provider secrets.
JSON.stringify(CryptoKey) loses the key material, making stored API keys undecryptable after
rotation and permanently replacing the key with {} after disable.
Code

services/storage/primaryProtectedStoreAdapters.ts[156]

+ reservedKeys: [ENCRYPTION_MIGRATION_JOURNAL_RECORD_KEY, PASSPHRASE_SENTINEL_RECORD_KEY],
Relevance

●● Moderate

Serious storage/encryption correctness claim; no close historical precedent found for excluding
CryptoKey records from migration adapters.

PR-#339

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new adapter excludes only the journal and sentinel, while APP_DATA_STORE also contains a
structured-cloned CryptoKey and API-key records. The migration encryption primitive serializes
arbitrary values with JSON, which cannot round-trip a CryptoKey needed by subsequent API-key
decryptions.

services/storage/primaryProtectedStoreAdapters.ts[148-162]
services/storage/idbKeyStore.ts[11-49]
services/storage/idbKeyStore.ts[201-247]
services/storage/storageEncryptionService.ts[161-189]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The generic app-data adapter rewrites unrelated metadata, including the structured-cloned local CryptoKey, through a JSON codec that cannot preserve CryptoKey material.
## Issue Context
Restrict this adapter to the protected `project` and `settings` records, or provide explicit lossless handling for every other app-data key. Lifecycle metadata and API-key storage records must not pass through `StorageEncryptionService.encrypt()`.
## Fix Focus Areas
- services/storage/primaryProtectedStoreAdapters.ts[148-162]
- services/storage/idbKeyStore.ts[11-49]
- services/storage/storageEncryptionService.ts[161-189]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Missing QNBS in EncryptionRecoveryModal📘 Rule violation§ Compliance
Description
components/settings/EncryptionRecoveryModal.tsx introduces new runtime logic but contains no
QNBS-v3 annotation comment in the diff. This violates the requirement to tag each non-trivial
logic change with a QNBS-v3 rationale comment.
Code

components/settings/EncryptionRecoveryModal.tsx[R25-28]

+export const EncryptionRecoveryModal: FC<Props> = ({ journal, onRecovered }) => {+ const { t } = useTranslation();+ const [sourcePassphrase, setSourcePassphrase] = useState('');+ const [targetPassphrase, setTargetPassphrase] = useState('');
Relevance

●●● Strong

Team often accepts adding required QNBS-v3 line annotations for new logic blocks/files.

PR-#339

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2524933 requires a QNBS-v3 annotation comment for each modified file with
substantive logic changes. The new EncryptionRecoveryModal component and its
state/effects/handlers are added without any QNBS-v3 marker anywhere in the file.

Rule 2524933: Require QNBS-v3 annotation comments on all non-trivial code changes
components/settings/EncryptionRecoveryModal.tsx[1-40]
components/settings/EncryptionRecoveryModal.tsx[140-192]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`components/settings/EncryptionRecoveryModal.tsx` adds substantive runtime logic but lacks any `QNBS-v3` annotation comment in the change.
## Issue Context
Compliance requires at least one `QNBS-v3` annotation comment per modified source file with non-trivial logic changes.
## Fix Focus Areas
- components/settings/EncryptionRecoveryModal.tsx[17-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Multi-line QNBS comment in JSX✓ Resolved📘 Rule violation⚙ Maintainability
Description
A QNBS-v3 annotation in PrivacySection.tsx spans multiple physical lines. This violates the
requirement that each QNBS-v3 comment be a single line.
Code

components/settings/PrivacySection.tsx[R116-117]

+ {/* QNBS-v3: both require an unlocked session key — clearIdbPassphrase/+ rotateIdbPassphrase re-migrate every protected store before touching the sentinel. */}
Relevance

●●● Strong

Strong precedent: team accepts collapsing multi-line QNBS-v3 annotations into required single-line
format.

PR-#286
PR-#293
PR-#287

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2525103 requires QNBS-v3 comments to occupy exactly one physical line. In
PrivacySection.tsx, the QNBS-v3 JSX comment is split across two lines (line wrap inside the `{/*
... */}` comment).

Rule 2525103: Limit QNBS-v3 comments to a single explanatory line
components/settings/PrivacySection.tsx[116-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
QNBS-v3 annotations must be exactly one physical line, but the added JSX QNBS-v3 comment spans multiple lines.
## Issue Context
Keeping QNBS-v3 comments single-line improves scanability and consistent parsing.
## Fix Focus Areas
- components/settings/PrivacySection.tsx[116-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. QNBS-v3 comment format invalid 📘 Rule violation⚙ Maintainability
Description
A // QNBS-v3: comment was added in App.tsx but does not follow the required bracketed `//
QNBS-v3: [reason / impact / creative value]` format. This reduces annotation consistency and
violates the QNBS-v3 format requirement.
Code

App.tsx[206]

+ // QNBS-v3: a durable, non-'completed' encryption migration journal means a disable/rotate was
Relevance

● Weak

Recent precedent rejected enforcing bracketed/single-line QNBS formatting updates.

PR-#339

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2524954 mandates that each // QNBS-v3 line comment exactly match `// QNBS-v3:
[reason / impact / creative value]. The added comment in App.tsx` is missing the required
bracketed three-part structure.

Rule 2524954: Enforce QNBS-v3 annotation format in TypeScript and JavaScript files
App.tsx[206-207]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`// QNBS-v3:` comments must use the exact format `// QNBS-v3: [reason / impact / creative value]`, but the added comment does not include the required bracketed segments.
## Issue Context
This rule applies to `.ts`/`.tsx`/`.js`/`.jsx` files and standardizes QNBS-v3 annotations.
## Fix Focus Areas
- App.tsx[206-207]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 116 rules
Review mode: 🧠 Deep: This is a high-risk, bug-dense encryption migration spanning many independent adapters, journal/recovery paths, IndexedDB concurrency, passphrase lifecycle, and production UI entry points; redundant review passes are materially valuable.

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment threadcomponents/settings/EncryptionRecoveryModal.tsx
Comment threadcomponents/settings/PrivacySection.tsx Outdated
Comment threadcomponents/settings/EncryptionRecoveryModal.tsx
Comment threadservices/storage/primaryProtectedStoreAdapters.ts
Comment threadservices/storage/storageEncryptionService.ts
Comment threadservices/storage/storageEncryptionService.ts
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Wire encryption migration journal into production disable/rotate + recovery UX

✨ Enhancement🐞 Bug fix🧪 Tests📝 Documentation🕐 40+ Minutes

Grey Divider

AI Description

• Wires the previously test-only encryption migration journal into real
clearIdbPassphrase()/rotateIdbPassphrase() entry points, replacing stubs that always threw.
• Adds new primary-store adapters (images, binder assets, codex, app-data, snapshots, RAG vectors)
and an encryptionMigrationOrchestrator combining them with existing secondary-store adapters.
• Adds EncryptionRecoveryModal + App.tsx startup guard to resume an interrupted disable/rotate
migration by re-deriving keys from re-entered passphrases.
• Extends PassphraseModal with 'disable'/'rotate' modes and a live migration progress bar;
adds Change-passphrase/Disable buttons to PrivacySection.
• Breaks a circular import (storageEncryptionService → orchestrator → adapters →
storageEncryptionService) via dynamic imports, matching repo convention.
Diagram

sequenceDiagram
actor User
participant UI as PassphraseModal / RecoveryModal
participant Svc as storageEncryptionService
participant Orch as encryptionMigrationOrchestrator
participant Adapters as Primary + Secondary Adapters
participant Journal as EncryptionMigrationJournal
User->>UI: Click Disable / Rotate
UI->>Svc: clearIdbPassphrase() / rotateIdbPassphrase()
Svc->>Orch: runProductionEncryptionMigration()
Orch->>Journal: beginEncryptionMigration()
Orch->>Adapters: migrateNext() batches + verify()
Adapters-->>Orch: progress callback
Orch-->>Svc: journal (committing)
Svc->>Journal: commit sentinel / clear journal
Note over User,Journal: If interrupted before 'completed',\nApp.tsx startup guard shows EncryptionRecoveryModal\nto resume via resumeEncryptionMigration()
Loading
High-Level Assessment

The PR's approach—reusing the existing journal/adapter abstraction from #337 and extending it with primary-store adapters plus a thin orchestrator—is the natural continuation of the stacked design and avoids duplicating migration/checkpoint/verification logic. Using dynamic imports to break the circular dependency (rather than restructuring module boundaries) matches an established repo convention (listenerMiddleware.ts, aiApi.ts), so no alternative architecture was warranted here.

Files changed (22) +2655 / -92

Enhancement (7) +193 / -29
App.tsxStartup recovery check takes priority over unlock modal+39/-5

Startup recovery check takes priority over unlock modal

• Reads the encryption migration journal on startup; if a non-'completed' journal exists, renders EncryptionRecoveryModal instead of IdbUnlockModal and clears the feature flag on disable recovery.

App.tsx

PassphraseModal.tsxAdd disable/rotate modes with progress bar+80/-22

Add disable/rotate modes with progress bar

• Extends PassphraseModalMode with 'disable'/'rotate', adds mode-specific titles/buttons/warnings, and renders a live migration progress bar driven by a new progress prop.

components/settings/PassphraseModal.tsx

PrivacySection.tsxAdd Change-passphrase and Disable buttons+10/-0

Add Change-passphrase and Disable buttons

• Wires new buttons to open the rotate/disable PassphraseModal modes and passes migration progress through.

components/settings/PrivacySection.tsx

useSettingsView.tsWire disable/rotate handlers with migration progress state+28/-0

Wire disable/rotate handlers with migration progress state

• Adds migrationProgress state and calls the real clearIdbPassphrase/rotateIdbPassphrase with a progress callback for the 'disable'/'rotate' passphrase modal branches.

hooks/useSettingsView.ts

encryptionMigrationJournal.tsExport journal record key for adapter reserved-key checks+3/-0

Export journal record key for adapter reserved-key checks

• Exposes ENCRYPTION_MIGRATION_JOURNAL_RECORD_KEY so primary adapters can skip the journal's own storage record during migration.

services/storage/encryptionMigrationJournal.ts

idbPassphraseSentinel.tsExport sentinel record key for adapter reserved-key checks+3/-0

Export sentinel record key for adapter reserved-key checks

• Exposes PASSPHRASE_SENTINEL_RECORD_KEY so the app-data adapter can avoid rewriting the sentinel record during migration.

services/storage/idbPassphraseSentinel.ts

protectedStoreMigration.tsAdd progress callback to migration runner+30/-2

Add progress callback to migration runner

• Introduces ProtectedStoreMigrationProgress/Callback types and threads progress reporting through the migrating/verifying phases of runProtectedStoreMigration.

services/storage/protectedStoreMigration.ts

Bug fix (1) +177 / -14
storageEncryptionService.tsReal clearIdbPassphrase/rotateIdbPassphrase/resumeEncryptionMigration+177/-14

Real clearIdbPassphrase/rotateIdbPassphrase/resumeEncryptionMigration

• Replaces the always-throwing stubs with real journal-backed disable/rekey implementations, adds resumeEncryptionMigration for recovery UX, and uses dynamic imports to break a circular dependency with the new orchestrator.

services/storage/storageEncryptionService.ts

Tests (6) +925 / -35
useSettingsView.test.tsTests for disable/rotate handlers and progress state+134/-2

Tests for disable/rotate handlers and progress state

• Adds coverage for clearIdbPassphrase/rotateIdbPassphrase calls, progress propagation, and failure handling in useSettingsView.

tests/unit/hooks/useSettingsView.test.ts

EncryptionRecoveryModal.test.tsxNew tests for EncryptionRecoveryModal+263/-0

New tests for EncryptionRecoveryModal

• Covers disable/rekey resume flows, recovery-required stuck state, field visibility, progress bar, and error handling.

tests/unit/settings/EncryptionRecoveryModal.test.tsx

PassphraseModal.test.tsxTests for new disable/rotate modes+191/-2

Tests for new disable/rotate modes

• Adds test suites for 'disable' and 'rotate' PassphraseModal modes covering titles, fields, validation, progress bar, and error states.

tests/unit/settings/PassphraseModal.test.tsx

PrivacySection.test.tsxTests for new Change-passphrase/Disable buttons+39/-1

Tests for new Change-passphrase/Disable buttons

• Adds assertions that the new buttons appear only when unlocked and correctly open the rotate/disable modal modes.

tests/unit/settings/PrivacySection.test.tsx

storageEncryptionService.test.tsReal migration round-trip and recovery tests+265/-15

Real migration round-trip and recovery tests

• Replaces fail-closed assertions with real success-path tests for clearIdbPassphrase/rotateIdbPassphrase, adds end-to-end round-trip tests for images, codex, binder assets, and RAG vectors, plus resumeEncryptionMigration recovery tests.

tests/unit/storage/storageEncryptionService.test.ts

storageEncryptionService.test.tsUpdate mocks and assertions for real disable/rotate behavior+33/-15

Update mocks and assertions for real disable/rotate behavior

• Updates mocked idbPassphraseSentinel module and rewrites assertions to reflect that clearIdbPassphrase/rotateIdbPassphrase now succeed instead of always throwing.

tests/unit/storageEncryptionService.test.ts

Documentation (3) +35 / -14
CLAUDE.mdDocument Phase 4 production wiring+2/-2

Document Phase 4 production wiring

• Updates internal engineering notes describing the shipped orchestrator, adapters, and recovery flow.

CLAUDE.md

IDB-ENCRYPTION.mdUpdate encryption docs to reflect completed Phase 4+32/-11

Update encryption docs to reflect completed Phase 4

• Rewrites status section and references to describe the shipped disable/rotate/recovery wiring and remaining E2E test gap.

docs/IDB-ENCRYPTION.md

0018-idb-encryption-lifecycle-and-recovery.mdMark ADR 0018 lifecycle operations as implemented+1/-1

Mark ADR 0018 lifecycle operations as implemented

• Updates ADR status to reflect that disable/rekey and recovery UX now ship.

docs/adr/0018-idb-encryption-lifecycle-and-recovery.md

Other (5) +1325 / -0
EncryptionRecoveryModal.tsxNew recovery modal for interrupted disable/rotate migrations+192/-0

New recovery modal for interrupted disable/rotate migrations

• New component that re-derives passphrase-based keys and resumes an interrupted migration via resumeEncryptionMigration(), showing live progress and a distinct non-resumable stuck state for 'recovery-required'.

components/settings/EncryptionRecoveryModal.tsx

encryptionMigrationOrchestrator.tsNew orchestrator combining primary + secondary adapters+84/-0

New orchestrator combining primary + secondary adapters

• New module providing runProductionEncryptionMigration and resumeProductionEncryptionMigration, the sole production callers of beginEncryptionMigration/runProtectedStoreMigration.

services/storage/encryptionMigrationOrchestrator.ts

primaryProtectedStoreAdapter.tsGeneric explicit-key/inline-keyPath adapter engine+315/-0

Generic explicit-key/inline-keyPath adapter engine

• New generic adapter factory for primary IndexedDB stores whose protected content is the whole stored value, handling batched cursor migration, concurrent-update detection, and verification.

services/storage/primaryProtectedStoreAdapter.ts

primaryProtectedStoreAdapters.tsConcrete adapters for images, app-data, snapshots, binder assets, codex+460/-0

Concrete adapters for images, app-data, snapshots, binder assets, codex

• Registers 5 concrete primary-store adapters built on the generic engine, handling per-store legacy/encrypted shape transforms and verification for enable/rekey/disable operations.

services/storage/primaryProtectedStoreAdapters.ts

ragVectorsProtectedStoreAdapter.tsBespoke per-project RAG vector migration adapter+274/-0

Bespoke per-project RAG vector migration adapter

• New adapter handling the RAG vectors store's aggregate-vs-individual-record duality per project, migrating and verifying atomically per project rather than per record.

services/storage/ragVectorsProtectedStoreAdapter.ts

qnbsand others added 2 commits August 12, 2026 22:06
…Y false positive
gitleaks' generic-api-key rule flags PASSPHRASE_SENTINEL_RECORD_KEY =
'idb_passphrase_sentinel_v1' (services/storage/idbPassphraseSentinel.ts,
mirrored as a mock constant in storageEncryptionService.test.ts) purely
because the variable name contains "PASSPHRASE" + "KEY" next to a
sufficiently long, structured string. It's a static IndexedDB record-key
identifier, not a credential — the app must know it deterministically to
find the sentinel record via store.get/put/delete.
Verified locally against the exact PR commit range with gitleaks 8.24.3
(matching CI): confirmed the finding reproduces without this config, and is
suppressed with it, while an unrelated real-looking secret (Stripe key
pattern) is still caught — the allowlist does not weaken detection elsewhere.
No .gitleaks.toml existed before this; [extend] useDefault = true keeps every
default rule active. Scoped to the exact leaked value only (not combined with
a path filter) since gitleaks' top-level [allowlist] ORs paths/regexes rather
than ANDing them — combining both would broaden the exemption to "anything in
that file" instead of narrowing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Independently flagged by CodeAnt, CodeRabbit, and qodo-code-review on PR #342:
1. Disable/rekey recovery can permanently strand an interrupted migration.
commitDisableMigration deletes the sentinel/salt BEFORE completing the
journal; commitRekeyMigration saves the new sentinel BEFORE completing the
journal. If completeEncryptionMigration() fails after either mutation, the
journal stays 'committing' but resumeEncryptionMigration's source-key
derivation now fails — the sentinel it needs is already gone (disable) or
already replaced (rekey) — permanently blocking recovery. resumeEncryptionMigration
now special-cases a journal already at 'committing': for disable, an absent
sentinel means the commit already ran — finish the journal bookkeeping
without deriving a key. For rekey, if the old passphrase no longer decrypts
the sentinel, verify the new passphrase against the journal's durable target
verifier (independent of the sentinel) before concluding the commit already
happened, instead of reporting a false wrong-passphrase error.
2. Blob optimistic-write check only compared size + MIME type, not bytes.
Two Blobs can share both while holding different content (e.g. a concurrent
binder-asset re-upload), so the migration's freshness check would silently
accept a stale value and overwrite a newer asset. valuesMatch is now async
and compares actual bytes for Blobs. writeBatch is split into a read phase
(readCurrentValues) and a write phase (putValues) so the async Blob
comparison never happens inside a live IDB transaction's request handler —
safe because the whole batch already runs under an exclusive
withMigrationAdmission() lock, so no writer can interleave between the two.
3. The app-data primary-store adapter corrupted IdbKeyStore records.
APP_DATA_STORE holds project/settings data AND IdbKeyStore's raw
non-extractable CryptoKey (local_crypto_key_v2) plus per-provider encrypted
API keys (api_key_<provider>_enc/_iv, legacy gemini_api_key_*). The adapter
only reserved the migration journal + passphrase sentinel keys, so
enabling/rotating/disabling encryption would JSON.stringify the CryptoKey
(destroying it) and re-encrypt the already-encrypted API-key bytes as if
they were plaintext project data — permanently losing every stored API key.
idbKeyStore.ts now exports isKeyStoreRecordKey(), wired into the app-data
adapter via a new isReservedKey predicate (reservedKeys alone can't express
the dynamic per-provider key family).
New regression coverage: 6 resumeEncryptionMigration committing-phase tests
(both operations × not-yet-committed / partially-committed sub-states), a
real disable+rekey round-trip proving a stored API key survives migration,
and 11 direct unit tests for valuesMatch's Blob comparison + isKeyStoreRecordKey.
All 98 pre-existing storageEncryptionService tests still pass unmodified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbsand others added 2 commits August 12, 2026 23:36
- ErrorBoundary: new `fallback` render-prop lets a boundary override the
default non-blocking ErrorFallback; wired into App.tsx's
EncryptionRecoveryModal boundary so a crash inside the recovery modal
still renders a blocking, focus-trapped Modal instead of leaving the
app underneath interactive during a pending disable/rekey recovery.
- Condense every wrapped QNBS-v3 comment introduced this PR (App.tsx,
EncryptionRecoveryModal.tsx) to the required single physical line.
- docs/adr/0018: fix stale "disable/rekey rejected outright" and
"cross-tab admission not shipped" claims to match the current
implementation.
- docs/IDB-ENCRYPTION.md: correct the protected-write call-path
description — save paths snapshot the key via resolveProtectedWriteKey()
then re-check only assertNoActiveEncryptionMigration() pre-transaction;
delete paths call assertIdbProtectedWriteAllowed() directly.
- tests/unit/storage/storageEncryptionService.test.ts: give every test a
fresh IDBFactory instead of one shared factory for the whole file, via
new _resetDbForTest()/_resetSentinelStoreForTest() exports alongside the
existing __resetEncryptionMigrationJournalConnectionsForTest() —
closes a latent cross-test leak where an encrypted record from one
test could outlive its salt/sentinel into a later test.
- tests/unit/hooks/useSettingsView.test.ts: add the rotate-failure
coverage mirroring the existing disable-failure test.
- tests/unit/ErrorBoundary.test.tsx: cover the new fallback prop.
Assessed as already resolved by earlier commits in this PR (no further
change needed, verified against current code):
- PassphraseModal.tsx's rotate-mode catch already distinguishes a
credential-mismatch IdbWrongPassphraseError (only ever thrown by the
pre-migration old-passphrase check) from any other migration/journal
failure, with test coverage for both paths.
- primaryProtectedStoreAdapter.ts's verify()/migrateNext() both skip
isReservedKey() records via readBatch(), so clearGeminiApiKey()/
clearApiKey() can no longer desync checkpoint.processed vs. verified —
isKeyStoreRecordKey() already excludes them from migration scope entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src-tauri/osv-scanner.toml: add the GHSA-jmr9-qjv8-65gv (extract-zip)
ignore entry already present on the sibling fix/333-download-progress-metrics
branch — no fixed version exists, transitive Playwright devDependency only.
- .gitguardian.yaml: extend the existing "Generic Password" false-positive
path exclusion to storageEncryptionService.test.ts, which pairs fake
sourcePassphrase/targetPassphrase fixture literals ('original', 'new-pass',
'wrong-old', ...) exercising the disable/rotate/recovery flows against
fake-indexeddb — same detector, same false-positive class already
documented for the locale trees above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qnbs

qnbs commented Aug 12, 2026

Copy link
Copy Markdown
OwnerAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
tests/unit/ErrorBoundary.test.tsx (1)

139-155: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use user-event for button interactions.

Lines 92 and 153 call fireEvent.click on buttons. Use userEvent.setup() and await user.click(...) in both tests. @testing-library/user-event is already available.

🤖 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 `@tests/unit/ErrorBoundary.test.tsx` around lines 139 - 155, Replace
fireEvent.click with asynchronous user-event interactions in both affected
button tests, including the test around the custom fallback reset callback.
Initialize userEvent.setup(), make the tests async, and await user.click(...)
while preserving the existing assertions.

Source: Path instructions

🤖 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 `@locales/el/settings.json`:
- Line 674: Update the Greek translation value for
settings.privacy.encryptionDisableAction from the current wording to
Απενεργοποίηση, preserving the existing key and JSON structure.
In `@locales/fr/settings.json`:
- Line 697: Update locales/fr/settings.json at lines 697-697 so the final
sentence names the migration as unable to resume. Apply equivalent wording in
locales/it/settings.json, then regenerate public/locales/it/bundle.json at lines
2343-2343; update both affected messages without changing other translation
content.
In `@locales/ko/settings.json`:
- Line 674: Correct the settings.privacy.encryptionDisableAction translation in
locales/ko/settings.json lines 674-674 to 비활성화 and locales/ru/settings.json
lines 674-674 to Отключить; update locales/el/settings.json to Απενεργοποίηση
and regenerate public/locales/el/bundle.json lines 2320-2320 from that source.
- Around line 678-679: Update the Korean at-rest encryption translations
associated with settings.privacy.encryptionDisabledStatus,
settings.privacy.encryptionEnabled, and the corresponding modal-title string to
consistently use 저장 시 암호화 instead of 미사용, preserving the existing meaning and
message structure.
In `@locales/pt/settings.json`:
- Line 239: Update the translation value for settings.ai.providerStatusReady
from the infinitive “Preparar” to the Portuguese ready-status term “Pronto”,
preserving the existing key.
In `@public/locales/fi/bundle.json`:
- Line 2326: Update the Finnish translations for encryptionForgotPassphrase,
encryptionPassphrase, encryptionChangeAction, encryptionChangeButton,
encryptionSetAction, and encryptionWarning to use “tunnuslause” consistently
instead of “salasana,” preserving the existing meaning and formatting.
In `@services/storage/storageEncryptionService.ts`:
- Around line 857-882: Restrict the existing try/catch around
deriveAndVerifySourceKeyFromSentinel so only source-passphrase verification can
enter the recovery branch. After successful verification, derive the target key
and call commitRekeyMigration outside that catch; only a source-verification
failure may validate the target verifier and invoke completeEncryptionMigration,
clearCompletedEncryptionMigration, and update the active-key state.
In `@tests/unit/storage/storageEncryptionService.test.ts`:
- Around line 1001-1025: Extend the failed rekey recovery test around
resumeEncryptionMigration to assert that the migration journal still exists and
hasPassphraseSentinel() remains true after rejection. Keep these checks in the
finally-safe flow before deleting the journal, so cleanup does not mask whether
failed recovery preserved both records.
---
Nitpick comments:
In `@tests/unit/ErrorBoundary.test.tsx`:
- Around line 139-155: Replace fireEvent.click with asynchronous user-event
interactions in both affected button tests, including the test around the custom
fallback reset callback. Initialize userEvent.setup(), make the tests async, and
await user.click(...) while preserving the existing assertions.
🪄 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

Run ID: 704e1664-306d-476a-aab3-feab2d8ff821

📥 Commits

Reviewing files that changed from the base of the PR and between 302de39 and 8a84048.

📒 Files selected for processing (59)
  • .gitleaks.toml
  • App.tsx
  • components/settings/EncryptionRecoveryModal.tsx
  • components/settings/PassphraseModal.tsx
  • components/settings/PrivacySection.tsx
  • components/ui/ErrorBoundary.tsx
  • docs/IDB-ENCRYPTION.md
  • docs/adr/0018-idb-encryption-lifecycle-and-recovery.md
  • locales/ar/settings.json
  • locales/de/settings.json
  • locales/el/settings.json
  • locales/en/settings.json
  • locales/es/settings.json
  • locales/eu/settings.json
  • locales/fa/settings.json
  • locales/fi/settings.json
  • locales/fr/settings.json
  • locales/he/settings.json
  • locales/hu/settings.json
  • locales/is/settings.json
  • locales/it/settings.json
  • locales/ja/settings.json
  • locales/ko/settings.json
  • locales/pt/settings.json
  • locales/ru/settings.json
  • locales/sv/settings.json
  • locales/zh/settings.json
  • public/locales/ar/bundle.json
  • public/locales/de/bundle.json
  • public/locales/el/bundle.json
  • public/locales/en/bundle.json
  • public/locales/es/bundle.json
  • public/locales/eu/bundle.json
  • public/locales/fa/bundle.json
  • public/locales/fi/bundle.json
  • public/locales/fr/bundle.json
  • public/locales/he/bundle.json
  • public/locales/hu/bundle.json
  • public/locales/is/bundle.json
  • public/locales/it/bundle.json
  • public/locales/ja/bundle.json
  • public/locales/ko/bundle.json
  • public/locales/pt/bundle.json
  • public/locales/ru/bundle.json
  • public/locales/sv/bundle.json
  • public/locales/zh/bundle.json
  • services/storage/idbKeyStore.ts
  • services/storage/idbPassphraseSentinel.ts
  • services/storage/index.ts
  • services/storage/primaryProtectedStoreAdapter.ts
  • services/storage/primaryProtectedStoreAdapters.ts
  • services/storage/ragVectorsProtectedStoreAdapter.ts
  • services/storage/storageEncryptionService.ts
  • tests/unit/ErrorBoundary.test.tsx
  • tests/unit/hooks/useSettingsView.test.ts
  • tests/unit/settings/EncryptionRecoveryModal.test.tsx
  • tests/unit/settings/PassphraseModal.test.tsx
  • tests/unit/storage/primaryProtectedStoreAdapter.test.ts
  • tests/unit/storage/storageEncryptionService.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • components/settings/PrivacySection.tsx
  • services/storage/ragVectorsProtectedStoreAdapter.ts
  • components/settings/PassphraseModal.tsx
  • services/storage/primaryProtectedStoreAdapter.ts
  • docs/IDB-ENCRYPTION.md
  • components/settings/EncryptionRecoveryModal.tsx
  • App.tsx
  • services/storage/primaryProtectedStoreAdapters.ts
  • tests/unit/settings/PassphraseModal.test.tsx

Comment threadlocales/el/settings.json Outdated
Comment threadlocales/fr/settings.json Outdated
Comment threadlocales/ko/settings.json Outdated
Comment threadlocales/ko/settings.json Outdated
Comment threadlocales/pt/settings.json Outdated
Comment threadpublic/locales/fi/bundle.json Outdated
Comment threadservices/storage/storageEncryptionService.ts
Comment threadtests/unit/storage/storageEncryptionService.test.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 (1)
src-tauri/osv-scanner.toml (1)

128-140: 🔒 Security & Privacy | 🔵 Trivial

Run the Tauri bundle workflow after this change.

The [[IgnoredVulns]] record is valid, and GHSA-jmr9-qjv8-65gv is the primary advisory ID for CVE-2026-56876. The file documents maintainer acceptance of the remaining ignore. Dispatch .github/workflows/tauri-build.yml and confirm Linux, Windows, and macOS bundles complete.

🤖 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 `@src-tauri/osv-scanner.toml` around lines 128 - 140, Run the Tauri bundle
workflow defined by tauri-build.yml after updating the IgnoredVulns entry, and
verify that the Linux, Windows, and macOS bundle jobs complete successfully.

Source: Coding guidelines

🤖 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 @.gitguardian.yaml:
- Around line 9-23: Remove the broad storageEncryptionService.test.ts entry from
secret.ignored-paths in the GitGuardian configuration. Replace the
false-positive fixture passphrases in the test with non-secret values where
possible, or add only their documented occurrences under secret.ignored_matches
using occurrence-scoped entries.
---
Nitpick comments:
In `@src-tauri/osv-scanner.toml`:
- Around line 128-140: Run the Tauri bundle workflow defined by tauri-build.yml
after updating the IgnoredVulns entry, and verify that the Linux, Windows, and
macOS bundle jobs complete successfully.
🪄 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

Run ID: 7d3f44bc-1ecd-4fef-85c0-fc1f68a79725

📥 Commits

Reviewing files that changed from the base of the PR and between 8a84048 and 73f6754.

📒 Files selected for processing (2)
  • .gitguardian.yaml
  • src-tauri/osv-scanner.toml

Comment thread.gitguardian.yaml
The IgnoredVulns entry itself was already added to
src-tauri/osv-scanner.toml in an earlier commit on this branch;
this adds the AUDIT.md documentation the repo's own housekeeping
policy requires for accepted dependency risk.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Aug 13, 2026
… commits
Resolves overlapping edits between this stacked i18n branch and #342's own
correction-loop work, which also touched locales/*/settings.json (both English
source wording and several locales' translations for the encryption
disable/rotate/recovery UI strings evolved independently after this branch
diverged).
Conflicts in the 19 locales/*/settings.json files were resolved with a
scripted 3-way per-key JSON merge (git's textual merge conflates adjacent
JSON lines into hunks even when only some individual keys actually conflict)
using this policy:
- Only one side changed a key from the merge base -> take that side.
- Both changed it identically, or only one side has a real (non-English)
translation -> take the real translation.
- Both sides have differing real translations -> take #342's (verified this
is not arbitrary: #342's non-English wording for every such key matches
#342's own updated English source exactly, confirming its correction loop
updated English + every translation together as one coherent unit, while
this branch was simply stale relative to that whole update).
- ar/he/fa (documented English-fallback RTL stubs) -> always track #342's
newer English source on conflict.
- The one key where this branch's content was genuinely unique and never
touched by #342 (settings.privacy.encryptionDisableConfirm's fuller,
more informative EN copy) was preserved.
All 19 public/locales/*/bundle.json files were regenerated fresh via
`pnpm run i18n:bundle` rather than hand-merged (2914 keys x 19 locales,
clean). Every other locale module file (characters/common/outline/worlds/
writer/etc.) this branch touches had no overlap with #342 and merged
automatically with no conflicts. All non-locale code/test/doc files from
#342's correction loop also merged automatically with no conflicts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
These 9 threads were still unresolved from an earlier review pass on this
branch, surfaced while preparing to merge all 5 open PRs:
- services/storage/storageEncryptionService.ts: fixed a real data-integrity
bug in resumeEncryptionMigration's committing-phase rekey recovery. The
try block wrapped deriveAndVerifySourceKeyFromSentinel,
deriveAndVerifyTargetKeyFromVerifier, AND commitRekeyMigration together, so
a commitRekeyMigration failure (after both passphrases had already verified
successfully) fell into the catch block, which then re-verified the target
passphrase, concluded "the commit must have already happened," and cleared
the journal — even though commitRekeyMigration had genuinely failed and the
durable sentinel could still hold the old passphrase while stores were
already re-encrypted under the new key. With no journal left, neither
passphrase could unlock the library. Isolated source-passphrase
verification into its own try/catch so only a genuine source-verify
failure enters the "maybe already committed" recovery branch; a
commitRekeyMigration failure now propagates as a retryable error with the
journal intact.
- tests/unit/storage/storageEncryptionService.test.ts: added the missing
hasPassphraseSentinel() assertion to the "rejects when neither passphrase
matches" test, matching its disable counterpart, so a regression that
clears the journal/sentinel on a failed recovery would fail this test.
Renamed all fixture passphrase literals ('original', 'new-pass',
'wrong-old', 'wrong-new') to an unambiguous 'test-fixture-*-passphrase'
pattern to reduce GitGuardian false-positive risk.
- .gitguardian.yaml: documented why the test file's path exclusion remains
(occurrence-scoped ignored_matches would be the narrower ideal, but
computing their SHA256 hashes requires the ggshield CLI + an authenticated
scan, unavailable in this environment) now that the fixture rename above
reduces the practical risk the exclusion covers.
- 7 locales (el, fi, fr, it, ko, pt, ru): fixed mistranslated or
inconsistent encryption-UI strings (Greek "disable" read as "make
incapable"; Korean used "unused" instead of "at rest" for encryption, and
a literal-translation "disable" verb instead of the idiomatic term; Russian
"disable" read as "forbid"; French/Italian's stuck-recovery message named
"the data" instead of "the migration" as unresumable; Finnish mixed
"salasana"/"tunnuslause" for passphrase inconsistently; Portuguese's AI
provider ready-status showed the infinitive "prepare" instead of "ready").
Rebuilt all 19 runtime bundles via `pnpm run i18n:check` (2914 keys x 19
locales, clean).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qnbs

qnbs commented Aug 13, 2026

Copy link
Copy Markdown
OwnerAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Aug 13, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

CI's doc-metrics drift gate (scripts/check-doc-metrics.mjs) caught this: the
i18n additions from this branch's own correction-loop work plus the merged-in
stacked i18n branch raised the key count from 2904 to 2914, but README.md's
four "2904 keys" mentions (badge + 3 prose references) were never updated to
match. Regenerated via `node scripts/sync-readme-metrics.mjs`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qnbs
qnbs enabled auto-merge (squash) August 13, 2026 03:19
@qnbs
qnbs merged commit 6822b41 into mainAug 13, 2026
27 of 29 checks passed
@qnbs
qnbs deleted the feat/encryption-migration-phase4-b branch August 13, 2026 04:27
qnbs added a commit that referenced this pull request Aug 13, 2026
PR #342 was squash-merged into main, creating a new commit distinct from the
individual commits this branch had already merged in from
feat/encryption-migration-phase4-b. This produced a fresh set of textual
conflicts in the 19 locales/*/settings.json files, but only ever the same
single key already resolved once before: settings.privacy.encryptionDisableConfirm.
This branch's own translation work (in all 19 languages, not just English)
gives it a fuller, more informative warning than main's terse placeholder —
kept this branch's version everywhere, matching the resolution already
applied to this same key in the prior merge commit.
Rebuilt all 19 public/locales/*/bundle.json bundles fresh via
`pnpm run i18n:check` (2914 keys x 19 locales, clean) rather than resolving
their conflicts by hand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Aug 13, 2026
…UI (#338) (#343)
* feat(storage): wire encryption migration journal into production disable/rotate (#338 items 1+2)
Replaces the clearIdbPassphrase/rotateIdbPassphrase stubs with real journal-backed
migrations: new primary-store adapters (images, binder assets, codex, app-data,
snapshots, RAG vectors) plug into the existing secondary-store adapters via a new
encryptionMigrationOrchestrator.ts, giving the previously test-only migration engine
its first production callers. Disable/rotate now migrate and verify every protected
store before touching the passphrase sentinel, and fail closed into the resumable
journal (not a partial verifier change) on error.
Breaks a circular import (storageEncryptionService -> orchestrator -> adapters ->
storageEncryptionService) via a dynamic import at the two call sites, matching this
codebase's existing dynamic-import convention for avoiding heavy/circular static
coupling (listenerMiddleware.ts, aiApi.ts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(settings): wire disable/rotate UI and startup recovery for at-rest encryption (#338 items 1+2)
Extends PassphraseModal with 'disable'/'rotate' modes (live migration progress bar),
adds Disable/Change-passphrase buttons to PrivacySection, and wires both through
useSettingsView to the real clearIdbPassphrase/rotateIdbPassphrase implementations.
Adds EncryptionRecoveryModal + an App.tsx startup check: if a disable/rotate journal
was interrupted (reload/crash) before reaching 'completed', it takes priority over the
normal unlock flow and lets the user re-enter their passphrase(s) to safely resume —
CryptoKey material is never persisted, only re-derivable. A 'recovery-required' journal
(the migration's own verification found an inconsistency) is surfaced as a distinct,
honest stuck state rather than a fake auto-fix.
Refactors clearIdbPassphrase/rotateIdbPassphrase's post-migration commit steps and
key/verifier derivation into shared helpers so resumeEncryptionMigration() (used by
the recovery UX) reaches the exact same end state as a fresh-start migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(storage,settings): cover the disable/rotate migration and recovery UX (#338)
Real-data round-trip tests (image, codex 3-shape dispatch, binder asset Blob<->bytes,
RAG vectors aggregate<->individual duality) through rekey then disable — the prior
disable/rotate tests only proved migration completes over empty stores. Adds
resumeEncryptionMigration coverage (correct resume, wrong source passphrase, missing
target passphrase for rekey), new EncryptionRecoveryModal component tests, and
disable/rotate mode + progress-bar tests for PassphraseModal, PrivacySection's new
Change/Disable buttons, and useSettingsView's handlePassphraseConfirm branches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: mark Phase 4 (#338) encryption disable/rotate/recovery as shipped
Updates CLAUDE.md § At-rest encryption and Known Technical Debt, docs/IDB-ENCRYPTION.md,
and ADR-0018's status line to reflect the production migration wiring implemented in
this branch — the orchestrator, primary-store adapters, protectedWriteAdmission cross-tab
gate, and EncryptionRecoveryModal. Remaining gap noted honestly: no E2E coverage yet for
the disable/rotate/recovery round trips (unit + component tests only so far).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* i18n: add settings.privacy.* keys for the encryption disable/rotate/recovery UI (#338)
9 new keys (disable-failed, migration-progress + label, recovery title/body/stuck/
resume, changed-status) added to en/settings.json and propagated to all 19 locales
via check-i18n-keys.mjs --fix. Real translations for the 5 production locales
(en + de/es/fr/it); the remaining 14 carry the English fallback per this repo's
tiered i18n convention. Rebuilds every public/locales/<lang>/bundle.json.
Stacked on feat/encryption-migration-phase4-b to keep that PR's code-only diff
under the ~100-file threshold the review bot needs to actually run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: update i18n key-count badge/table/prose to 2913 (was 2904)
Fixes the doc-metrics drift check.mjs flagged after adding 9 settings.privacy.* keys
for the encryption disable/rotate/recovery UI in the previous commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore(security): gitleaks allowlist for PASSPHRASE_SENTINEL_RECORD_KEY false positive
gitleaks' generic-api-key rule flags PASSPHRASE_SENTINEL_RECORD_KEY =
'idb_passphrase_sentinel_v1' (services/storage/idbPassphraseSentinel.ts,
mirrored as a mock constant in storageEncryptionService.test.ts) purely
because the variable name contains "PASSPHRASE" + "KEY" next to a
sufficiently long, structured string. It's a static IndexedDB record-key
identifier, not a credential — the app must know it deterministically to
find the sentinel record via store.get/put/delete.
Verified locally against the exact PR commit range with gitleaks 8.24.3
(matching CI): confirmed the finding reproduces without this config, and is
suppressed with it, while an unrelated real-looking secret (Stripe key
pattern) is still caught — the allowlist does not weaken detection elsewhere.
No .gitleaks.toml existed before this; [extend] useDefault = true keeps every
default rule active. Scoped to the exact leaked value only (not combined with
a path filter) since gitleaks' top-level [allowlist] ORs paths/regexes rather
than ANDing them — combining both would broaden the exemption to "anything in
that file" instead of narrowing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(storage): 3 critical encryption-migration bugs found in PR review
Independently flagged by CodeAnt, CodeRabbit, and qodo-code-review on PR #342:
1. Disable/rekey recovery can permanently strand an interrupted migration.
commitDisableMigration deletes the sentinel/salt BEFORE completing the
journal; commitRekeyMigration saves the new sentinel BEFORE completing the
journal. If completeEncryptionMigration() fails after either mutation, the
journal stays 'committing' but resumeEncryptionMigration's source-key
derivation now fails — the sentinel it needs is already gone (disable) or
already replaced (rekey) — permanently blocking recovery. resumeEncryptionMigration
now special-cases a journal already at 'committing': for disable, an absent
sentinel means the commit already ran — finish the journal bookkeeping
without deriving a key. For rekey, if the old passphrase no longer decrypts
the sentinel, verify the new passphrase against the journal's durable target
verifier (independent of the sentinel) before concluding the commit already
happened, instead of reporting a false wrong-passphrase error.
2. Blob optimistic-write check only compared size + MIME type, not bytes.
Two Blobs can share both while holding different content (e.g. a concurrent
binder-asset re-upload), so the migration's freshness check would silently
accept a stale value and overwrite a newer asset. valuesMatch is now async
and compares actual bytes for Blobs. writeBatch is split into a read phase
(readCurrentValues) and a write phase (putValues) so the async Blob
comparison never happens inside a live IDB transaction's request handler —
safe because the whole batch already runs under an exclusive
withMigrationAdmission() lock, so no writer can interleave between the two.
3. The app-data primary-store adapter corrupted IdbKeyStore records.
APP_DATA_STORE holds project/settings data AND IdbKeyStore's raw
non-extractable CryptoKey (local_crypto_key_v2) plus per-provider encrypted
API keys (api_key_<provider>_enc/_iv, legacy gemini_api_key_*). The adapter
only reserved the migration journal + passphrase sentinel keys, so
enabling/rotating/disabling encryption would JSON.stringify the CryptoKey
(destroying it) and re-encrypt the already-encrypted API-key bytes as if
they were plaintext project data — permanently losing every stored API key.
idbKeyStore.ts now exports isKeyStoreRecordKey(), wired into the app-data
adapter via a new isReservedKey predicate (reservedKeys alone can't express
the dynamic per-provider key family).
New regression coverage: 6 resumeEncryptionMigration committing-phase tests
(both operations × not-yet-committed / partially-committed sub-states), a
real disable+rekey round-trip proving a stored API key survives migration,
and 11 direct unit tests for valuesMatch's Blob comparison + isKeyStoreRecordKey.
All 98 pre-existing storageEncryptionService tests still pass unmodified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(encryption): address remaining PR #342 review findings
- ErrorBoundary: new `fallback` render-prop lets a boundary override the
default non-blocking ErrorFallback; wired into App.tsx's
EncryptionRecoveryModal boundary so a crash inside the recovery modal
still renders a blocking, focus-trapped Modal instead of leaving the
app underneath interactive during a pending disable/rekey recovery.
- Condense every wrapped QNBS-v3 comment introduced this PR (App.tsx,
EncryptionRecoveryModal.tsx) to the required single physical line.
- docs/adr/0018: fix stale "disable/rekey rejected outright" and
"cross-tab admission not shipped" claims to match the current
implementation.
- docs/IDB-ENCRYPTION.md: correct the protected-write call-path
description — save paths snapshot the key via resolveProtectedWriteKey()
then re-check only assertNoActiveEncryptionMigration() pre-transaction;
delete paths call assertIdbProtectedWriteAllowed() directly.
- tests/unit/storage/storageEncryptionService.test.ts: give every test a
fresh IDBFactory instead of one shared factory for the whole file, via
new _resetDbForTest()/_resetSentinelStoreForTest() exports alongside the
existing __resetEncryptionMigrationJournalConnectionsForTest() —
closes a latent cross-test leak where an encrypted record from one
test could outlive its salt/sentinel into a later test.
- tests/unit/hooks/useSettingsView.test.ts: add the rotate-failure
coverage mirroring the existing disable-failure test.
- tests/unit/ErrorBoundary.test.tsx: cover the new fallback prop.
Assessed as already resolved by earlier commits in this PR (no further
change needed, verified against current code):
- PassphraseModal.tsx's rotate-mode catch already distinguishes a
credential-mismatch IdbWrongPassphraseError (only ever thrown by the
pre-migration old-passphrase check) from any other migration/journal
failure, with test coverage for both paths.
- primaryProtectedStoreAdapter.ts's verify()/migrateNext() both skip
isReservedKey() records via readBatch(), so clearGeminiApiKey()/
clearApiKey() can no longer desync checkpoint.processed vs. verified —
isKeyStoreRecordKey() already excludes them from migration scope entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): resolve OSV and GitGuardian findings on PR #342's branch
- src-tauri/osv-scanner.toml: add the GHSA-jmr9-qjv8-65gv (extract-zip)
ignore entry already present on the sibling fix/333-download-progress-metrics
branch — no fixed version exists, transitive Playwright devDependency only.
- .gitguardian.yaml: extend the existing "Generic Password" false-positive
path exclusion to storageEncryptionService.test.ts, which pairs fake
sourcePassphrase/targetPassphrase fixture literals ('original', 'new-pass',
'wrong-old', ...) exercising the disable/rotate/recovery flows against
fake-indexeddb — same detector, same false-positive class already
documented for the locale trees above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(i18n): translate the disable-encryption warning across all locales
settings.privacy.encryptionDisableConfirm was updated in English to a
detailed warning ("This will decrypt all your data and turn off at-rest
encryption...") but the other 18 locales still carried the old short
"Disable encryption" label. Since PassphraseModal renders this key as the
disable-mode warning paragraph (not a button label), non-English users
were seeing a misleading warning for a sensitive, irreversible-feeling
action. Translated properly for all 18 remaining locales and rebuilt bundles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: document the extract-zip OSV ignore accepted-risk in AUDIT.md
The IgnoredVulns entry itself was already added to
src-tauri/osv-scanner.toml in an earlier commit on this branch;
this adds the AUDIT.md documentation the repo's own housekeeping
policy requires for accepted dependency risk.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: address 9 remaining CodeRabbit findings on PR #342
These 9 threads were still unresolved from an earlier review pass on this
branch, surfaced while preparing to merge all 5 open PRs:
- services/storage/storageEncryptionService.ts: fixed a real data-integrity
bug in resumeEncryptionMigration's committing-phase rekey recovery. The
try block wrapped deriveAndVerifySourceKeyFromSentinel,
deriveAndVerifyTargetKeyFromVerifier, AND commitRekeyMigration together, so
a commitRekeyMigration failure (after both passphrases had already verified
successfully) fell into the catch block, which then re-verified the target
passphrase, concluded "the commit must have already happened," and cleared
the journal — even though commitRekeyMigration had genuinely failed and the
durable sentinel could still hold the old passphrase while stores were
already re-encrypted under the new key. With no journal left, neither
passphrase could unlock the library. Isolated source-passphrase
verification into its own try/catch so only a genuine source-verify
failure enters the "maybe already committed" recovery branch; a
commitRekeyMigration failure now propagates as a retryable error with the
journal intact.
- tests/unit/storage/storageEncryptionService.test.ts: added the missing
hasPassphraseSentinel() assertion to the "rejects when neither passphrase
matches" test, matching its disable counterpart, so a regression that
clears the journal/sentinel on a failed recovery would fail this test.
Renamed all fixture passphrase literals ('original', 'new-pass',
'wrong-old', 'wrong-new') to an unambiguous 'test-fixture-*-passphrase'
pattern to reduce GitGuardian false-positive risk.
- .gitguardian.yaml: documented why the test file's path exclusion remains
(occurrence-scoped ignored_matches would be the narrower ideal, but
computing their SHA256 hashes requires the ggshield CLI + an authenticated
scan, unavailable in this environment) now that the fixture rename above
reduces the practical risk the exclusion covers.
- 7 locales (el, fi, fr, it, ko, pt, ru): fixed mistranslated or
inconsistent encryption-UI strings (Greek "disable" read as "make
incapable"; Korean used "unused" instead of "at rest" for encryption, and
a literal-translation "disable" verb instead of the idiomatic term; Russian
"disable" read as "forbid"; French/Italian's stuck-recovery message named
"the data" instead of "the migration" as unresumable; Finnish mixed
"salasana"/"tunnuslause" for passphrase inconsistently; Portuguese's AI
provider ready-status showed the infinitive "prepare" instead of "ready").
Rebuilt all 19 runtime bundles via `pnpm run i18n:check` (2914 keys x 19
locales, clean).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: sync README i18n key-count badges to 2914 (was stale at 2904)
CI's doc-metrics drift gate (scripts/check-doc-metrics.mjs) caught this: the
i18n additions from this branch's own correction-loop work plus the merged-in
stacked i18n branch raised the key count from 2904 to 2914, but README.md's
four "2904 keys" mentions (badge + 3 prose references) were never updated to
match. Regenerated via `node scripts/sync-readme-metrics.mjs`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Aug 13, 2026
Real conflicts (not just textual noise) resolved:
- hooks/useSettingsView.ts + tests/unit/hooks/useSettingsView.test.ts: this
branch's D5 useMemo-wrapped context-value fix was missing `migrationProgress`
from its returned object — main's independently-added encryption feature had
it, this branch's earlier refactor didn't carry it forward. Merged both: kept
the useMemo optimization, added migrationProgress to the memoized object and
its dependency array. Test mocks reconciled the same way — kept this branch's
stable-toast pattern (the actual regression test for the D5 fix) while
aliasing mockToastInfo/mockToastSuccess to stableToast's own methods so the
existing encryption tests' assertions against those names keep working.
- CHANGELOG.md: two genuinely distinct "### Fixed" entries from different PRs
(#332's desktop boot/SettingsView fixes vs. #341/#344's AI Writing Studio
readability fixes) — kept both.
- README.md + locales/*/settings.json (el/fi/hu/is/pt/sv) + their bundles:
mix of stale i18n key-count badges (regenerated via
`node scripts/sync-readme-metrics.mjs` post-merge rather than guessing) and
the same encryptionSetupFailed/providerStatusReady translation conflicts
already resolved once in the previous merge commit — reapplied the same
per-key resolution (whichever side has the real, non-English translation).
Rebuilt all 19 bundles fresh via `pnpm run i18n:check` (2915 keys x 19
locales, clean) rather than resolving bundle.json conflicts by hand.
- src-tauri/osv-scanner.toml: cosmetic-only comment-header conflict (both
sides already had the identical extract-zip IgnoredVulns entry) — kept the
more current review-date comment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Aug 13, 2026
Real conflicts resolved:
- CHANGELOG.md: three genuinely distinct "### Fixed" entries from different
PRs (#341/#344's AI Writing Studio readability fixes, already in main, vs.
this branch's own voice-download-progress-scale fix) — kept both.
- locales/pt/settings.json + its bundle: same providerStatusReady
("Preparar" infinitive vs. the already-fixed "Pronto") conflict already
resolved on the other branches this session — took main's already-correct
side, since this branch never touched that key.
- README.md: stale i18n key-count/test-file-count badges — regenerated via
`node scripts/sync-readme-metrics.mjs` post-merge rather than guessing
(2918 keys × 19 locales, 547 test files).
- src-tauri/osv-scanner.toml: cosmetic-only comment-header conflict (both
sides already had the identical extract-zip IgnoredVulns entry) — kept the
more current review-date comment.
Rebuilt all 19 public/locales/*/bundle.json bundles fresh via
`pnpm run i18n:check` rather than resolving bundle.json conflicts by hand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Aug 13, 2026
…ontradictory local-only doc
- CHANGELOG.md: the [1.27.0] section never documented its own headline
feature (#342/#343's disable-encryption/passphrase-rotation production
wiring) and still carried the prior release's "intentionally unavailable
... see issue #338" caveat, directly contradicting what shipped in this
same version (chatgpt-codex-connector). Added the missing entry, qualified
the stale caveat as describing that point in time, and corrected the Docs
section's "opened issue #338" line to note it closed in this release.
- docs/DEEPSOURCE-REVIEW-LOOP.md: two restored local-only tracking commits
(originally discarded by an earlier `git reset --hard`, recovered via
cherry-pick) carried "LOCAL-ONLY — not pushed to remote" / "do not push
this entry upstream" wording that became false the moment they were
committed to a pushed branch (qodo-code-review, coderabbitai). Removed the
contradictory local-only framing, keeping the content as a normal dated
log entry. Also softened the JS-0440 remediation TODO's "consider a
rule-level ignore repo-wide" suggestion per coderabbitai's security
concern — a blanket ignore would hide future unsafe
`dangerouslySetInnerHTML` uses, not just the one reviewed occurrence.
The public/sw.js QNBS-v3-comment findings (qodo-code-review, coderabbitai)
were false positives — the existing single-line QNBS-v3 comment on the line
directly above APP_VERSION was already unchanged by this PR's one-line
version-bump diff; verified via `git diff main~1 -- public/sw.js`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Aug 13, 2026
…reliability fixes (#351)
* docs(deepsource): log re-surfaced JS-0440 finding (local-only, not for push)
Co-Authored-By: GitHub Copilot (Claude Sonnet 5) <noreply@github.com>
* docs(deepsource): correct stale JS-0440 disposition, re-surfaced 2026-08-01 (local-only)
Co-Authored-By: GitHub Copilot (Claude Sonnet 5) <noreply@github.com>
* release: v1.27.0 — Phase 4 encryption production wiring + desktop/AI reliability fixes
Closes out issue #338 (Phase 4 of the at-rest encryption lifecycle): disable
encryption and passphrase rotation are now live in Settings › Privacy,
backed by the durable resumable migration journal built in the prior
release. Also ships three independently-diagnosed reliability/UX fixes:
- Tauri desktop cold boot never read persisted state back (#332) — every
desktop launch loaded as a brand-new user regardless of what was actually
saved to disk; boot hydration now mirrors the already-correct save path.
Quitting also now awaits any pending debounced autosave instead of risking
a mid-debounce data loss.
- AI Writing Studio manuscript text was unreadable, with the caret/selection
visually drifting from the real text (#341) — a blur/font-mismatch/
scroll-desync defect in the invisible-input-over-visible-mirror rendering
pattern used by both Writer Studio and the main manuscript editor.
- Voice and WebLLM model download progress bars showed real byte counts and
transfer speed instead of a bare percentage, and the voice download bar's
progress-scale bug (stuck at ~95% for most of the download) is fixed
(#333 item 1).
All 5 correction-loop PRs (#342-#346) ran to full quiescence before merging
— every CodeRabbit/CodeAnt/Qodo/Sourcery finding fixed or justified with
evidence, 0 unresolved review threads, full CI green (Quality Gate, E2E, E2E
Deep Coverage, Build, Storybook, Lighthouse, Visual Regression) — including
a genuine data-integrity bug found and fixed during that loop: a shared
try/catch in the rekey-resume recovery path could, on a crash immediately
after `commitRekeyMigration`, misinterpret that crash as "already committed"
and clear the migration journal while the durable sentinel still held the
old passphrase — leaving neither passphrase able to unlock the library.
2919 i18n keys × 19 locales.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: address PR #351 review findings — stale CHANGELOG claim + self-contradictory local-only doc
- CHANGELOG.md: the [1.27.0] section never documented its own headline
feature (#342/#343's disable-encryption/passphrase-rotation production
wiring) and still carried the prior release's "intentionally unavailable
... see issue #338" caveat, directly contradicting what shipped in this
same version (chatgpt-codex-connector). Added the missing entry, qualified
the stale caveat as describing that point in time, and corrected the Docs
section's "opened issue #338" line to note it closed in this release.
- docs/DEEPSOURCE-REVIEW-LOOP.md: two restored local-only tracking commits
(originally discarded by an earlier `git reset --hard`, recovered via
cherry-pick) carried "LOCAL-ONLY — not pushed to remote" / "do not push
this entry upstream" wording that became false the moment they were
committed to a pushed branch (qodo-code-review, coderabbitai). Removed the
contradictory local-only framing, keeping the content as a normal dated
log entry. Also softened the JS-0440 remediation TODO's "consider a
rule-level ignore repo-wide" suggestion per coderabbitai's security
concern — a blanket ignore would hide future unsafe
`dangerouslySetInnerHTML` uses, not just the one reviewed occurrence.
The public/sw.js QNBS-v3-comment findings (qodo-code-review, coderabbitai)
were false positives — the existing single-line QNBS-v3 comment on the line
directly above APP_VERSION was already unchanged by this PR's one-line
version-bump diff; verified via `git diff main~1 -- public/sw.js`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: GitHub Copilot (Claude Sonnet 5) <noreply@github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLThis PR changes 1000+ lines, ignoring generated files

Projects

None yet

1 participant

@qnbs