Skip to content

feat: fix cross-device sync with E2EE key hierarchy and docs cleanup - #177

Merged
github-actions[bot] merged 1 commit into
developfrom
feature/sync-e2ee-and-docs-cleanup
Mar 29, 2026
Merged

feat: fix cross-device sync with E2EE key hierarchy and docs cleanup#177
github-actions[bot] merged 1 commit into
developfrom
feature/sync-e2ee-and-docs-cleanup

Conversation

@tomymaritano

@tomymaritano tomymaritano commented Mar 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix sync: Add missing DB migrations (tag_sync_log, notebook_sync_log), E2EE key hierarchy (passphrase → PBKDF2 → AES-KW wrapped CEK), deep link auth for Windows/Linux, and auto-trial on signup
  • Clean docs: Delete 3 stale session artifacts, archive 26 historical plans, update SECURITY.md versions, reorganize docs structure

What was broken

Sync failed on all platforms due to 4 cascading issues:

  1. Server 500s — tag_sync_log and notebook_sync_log tables never created in production
  2. Cross-device decrypt failure — each device generated its own AES-256 key
  3. Desktop auth broken — magic link opened browser, not the Electron app
  4. 403 on sync — no subscription created on signup, sync gated behind Pro

Key changes

Sync & E2EE

  • 3 new DB migrations (0005, 0006, 0007) + user_keys table for key hierarchy
  • EncryptionService refactored: PBKDF2 key derivation, AES-KW wrap/unwrap, recovery key, legacy key migration
  • GET/POST /sync/keys endpoints for cross-device key exchange
  • 7 new IPC handlers for encryption key management
  • requestSingleInstanceLock + second-instance handler for Windows/Linux deep links
  • readied:// deep link URLs for desktop magic link emails
  • Auto-create 14-day trial subscription on first POST /auth/verify

Docs cleanup

  • Deleted: SEMANA_2_COMPLETE.md, BACKEND_INTEGRATION_COMPLETE.md, TESTING_SYNC.md
  • Archived: 20 plan files + 6 superpowers specs → docs/archived/
  • Moved: PLUGIN_SYSTEM.mddocs/, TODO_MONITORING.mdOBSERVABILITY.md
  • Updated: SECURITY.md supported versions (0.2.x → 0.9.x)

Test plan

  • pnpm typecheck — 17/17 tasks pass
  • pnpm test — 42/42 tests pass
  • Manual: Mac creates note → sync → Windows pulls → note appears
  • Manual: Click magic link on Windows → Electron app opens and authenticates
  • Manual: New user signup → GET /sync/status returns enabled: true

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • End-to-end encryption setup with passphrase and recovery key support for enhanced account security
    • Deep-link authentication enabling seamless multi-device login on desktop
    • Automatic subscription provisioning during account creation
  • Documentation

    • Updated security support information for version 0.9.x and later
    • Activated observability documentation

…and auto-trial

Sync was broken on all platforms due to 4 cascading issues:
1. Missing DB migrations for tag_sync_log and notebook_sync_log tables (server 500s)
2. Per-device encryption keys preventing cross-device decryption
3. Magic link emails opening browser instead of desktop app
4. No subscription created on signup (sync gated behind Pro)

Changes:
- Add migrations 0005 (sync tables), 0006 (shared_notes columns), 0007 (user_keys)
- Implement E2EE key hierarchy: passphrase → PBKDF2 → Master Key → AES-KW wrapped CEK
- Add GET/POST /sync/keys endpoints for key exchange
- Refactor EncryptionService with key derivation, wrap/unwrap, recovery key, legacy migration
- Add requestSingleInstanceLock + second-instance handler for Windows/Linux deep links
- Send readied:// deep link URLs for desktop magic link emails
- Auto-create 14-day trial subscription on first auth verify
- Clean up 29 stale doc artifacts, archive 26 historical plans, update SECURITY.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 29, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
readide Ready Ready Preview, Comment Mar 29, 2026 8:24pm

Request Review

@coderabbitai

coderabbitai Bot commented Mar 29, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements end-to-end encryption key management across desktop and API layers. Changes include database migrations for user encryption keys and sync logs, new E2EE key API endpoints, desktop encryption service refactoring for key lifecycle operations, IPC handlers for encryption flows, magic-link client differentiation for deep links, subscription auto-provisioning, and legacy documentation cleanup.

Changes

Cohort / File(s) Summary
Documentation Cleanup
BACKEND_INTEGRATION_COMPLETE.md, SEMANA_2_COMPLETE.md, TESTING_SYNC.md, SECURITY.md
Removed legacy backend integration, bidirectional sync, and testing documentation files; updated security policy version support from 0.2.x to 0.9.x.
Database Schema & Migrations
packages/api/drizzle/0005_sync_tables.sql, packages/api/drizzle/0006_shared_notes_columns.sql, packages/api/drizzle/0007_user_keys.sql, packages/api/drizzle/meta/_journal.json, packages/api/src/db/schema.ts
Added tag_sync_log and notebook_sync_log tables, extended shared_notes with metadata columns, and introduced user_keys table for E2EE key storage (salt, wrapped CEK, KDF parameters).
API Key Management Routes
packages/api/src/routes/sync.ts, packages/api/src/routes/auth.ts
Added GET/POST /sync/keys endpoints for key retrieval and storage; extended auth to differentiate web vs. desktop clients for deep-link generation; added automatic trial subscription provisioning on first login.
Desktop Encryption Service
apps/desktop/src/main/services/encryptionService.ts
Refactored initialization to load/cache CEK from persistent storage with fallback to legacy key; added key setup with PBKDF2 passphrase derivation, unlock flows (passphrase and recovery key), passphrase change, and legacy key migration functionality.
Desktop API Client & IPC
apps/desktop/src/main/services/apiClient.ts, apps/desktop/src/main/index.ts
Extended ApiClient with E2EE key management methods (getEncryptionKeys, setEncryptionKeys); added client field to magic-link requests; implemented IPC handlers for encryption key lifecycle (readiness check, key status, setup, unlock, passphrase change, migration); added Windows/Linux single-instance lock with deep-link forwarding via readied:// protocol.
Desktop Preload API
apps/desktop/src/preload/index.ts
Extended encryption API surface with IPC-backed methods: isReady, getKeyStatus, setupKeys, unlockWithPassphrase, unlockWithRecoveryKey, migrateLegacyKey, changePassphrase with corresponding type definitions.
Observability Docs
packages/api/docs/OBSERVABILITY.md
Updated placeholder documentation with status callout indicating Sentry setup is not yet implemented.

Sequence Diagram(s)

sequenceDiagram
    participant Desktop as Desktop App
    participant Main as Main Process
    participant EncSvc as EncryptionService
    participant ApiClient as ApiClient
    participant API as Backend API
    participant DB as Database

    Desktop->>Main: setupKeys(passphrase)
    Main->>EncSvc: setupKeys(passphrase)
    EncSvc->>EncSvc: Generate CEK & derive MK via PBKDF2
    EncSvc->>EncSvc: Wrap CEK & recovery key
    EncSvc->>EncSvc: Cache CEK locally
    EncSvc-->>Main: KeySetupResult{salt, wrappedCek, recoveryKey}
    Main->>ApiClient: setEncryptionKeys(salt, wrappedCek, kdfParams)
    ApiClient->>API: POST /sync/keys
    API->>DB: upsert user_keys
    API-->>ApiClient: {success: true}
    ApiClient-->>Main: success
    Main-->>Desktop: {success, recoveryKey}
Loading
sequenceDiagram
    participant NewDevice as New Device
    participant Main as Main Process
    participant EncSvc as EncryptionService
    participant ApiClient as ApiClient
    participant API as Backend API

    NewDevice->>Main: unlockWithPassphrase(passphrase)
    Main->>ApiClient: getEncryptionKeys()
    ApiClient->>API: GET /sync/keys
    API-->>ApiClient: {salt, wrappedCek, kdfParams}
    ApiClient-->>Main: keyBundle
    Main->>EncSvc: unlockWithPassphrase(passphrase, salt, wrappedCek, kdfParams)
    EncSvc->>EncSvc: Derive MK from passphrase + salt via PBKDF2
    EncSvc->>EncSvc: Unwrap CEK using derived MK
    EncSvc->>EncSvc: Cache CEK locally
    EncSvc-->>Main: success/wrongPassphrase
    Main-->>NewDevice: {success, wrongPassphrase?}
Loading
sequenceDiagram
    participant User as User
    participant DeepLink as Deep Link Handler
    participant App1 as Instance 1 (Running)
    participant App2 as Instance 2 (Launched)
    participant MainWindow as Main Window

    User->>DeepLink: Open readied://auth/verify?token=...
    DeepLink->>App2: Launch with readied:// URL
    App2->>App2: requestSingleInstanceLock()
    App2-->>App2: Lock fails (App1 has it)
    App2->>App1: second-instance event + URL
    App1->>App1: Parse token from URL
    App1->>MainWindow: auth:verify-token IPC
    MainWindow->>MainWindow: Process auth
    App1->>App1: Focus & restore window
    App2->>App2: Exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #150: Modifies desktop ApiClient request/retry logic alongside E2EE key API additions in this PR.
  • #155: Release PR that overlaps with encryption service, IPC handlers, and sync route modifications in this changeset.
  • #149: Related PR touching encryption/sync codepaths, user_keys migration, and desktop API client changes.

Suggested labels

app:desktop, package:api, size/XL

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main components of this changeset: implementing E2EE key hierarchy for cross-device sync and cleaning up documentation. It is specific, concise, and reflects the primary changes without unnecessary noise.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/sync-e2ee-and-docs-cleanup

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

❤️ Share

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

@github-actions
github-actions Bot merged commit fb0dafc into develop Mar 29, 2026
14 of 15 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09501f15c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +679 to +682
salt,
wrappedCek,
wrappedCekRecovery: wrappedCekRecovery ?? null,
kdfParams: JSON.stringify(kdfParams),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve recovery-wrapped CEK on passphrase change

sync.post('/keys') always writes wrappedCekRecovery: wrappedCekRecovery ?? null during upsert, so updates that omit this field erase any previously stored recovery-wrapped CEK. The new desktop encryption:changePassphrase flow sends only salt, wrappedCek, and kdfParams, which means a normal passphrase change silently clears recovery data and makes unlockWithRecoveryKey unusable afterward.

Useful? React with 👍 / 👎.

Comment on lines +2766 to +2769
app.on('second-instance', (_event, commandLine) => {
const log = getLogger();
// On Windows, the deep link URL is the last argument
const deepLinkUrl = commandLine.find(arg => arg.startsWith('readied://'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Parse deep-link args on first Windows/Linux instance launch

The Windows/Linux deep-link token is only extracted inside the second-instance handler. If the app is closed and the user clicks a magic link, Electron starts a first instance with readied://... in startup args, but this code path never reads initial process.argv, so the auth token is dropped and verification does not proceed unless an instance was already running.

Useful? React with 👍 / 👎.

Comment on lines 110 to 113
}

return false; // No key available — passphrase setup required
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Initialize a CEK when no cached key exists

initialize() now returns false when neither cache nor legacy key exists, instead of creating a key. Startup still continues after awaiting initialization, so fresh installs can run with this.key === null; subsequent sync encrypt/decrypt operations throw Encryption key not initialized, which breaks first-device sync until some separate setup flow populates a key.

Useful? React with 👍 / 👎.

github-actions Bot pushed a commit that referenced this pull request Mar 29, 2026
## Summary

Fixes 3 issues flagged by Codex bot review on PR #177.

## Changes

### 1. Preserve recovery key on passphrase change
The `POST /sync/keys` upsert was overwriting `wrappedCekRecovery` with
`null` when not provided (e.g., during passphrase change). Now only
overwrites if explicitly sent.

### 2. Parse deep link on cold start (Windows/Linux)
When the app isn't running and user clicks a `readied://` magic link,
the OS launches a new instance with the URL in `process.argv`.
Previously only `second-instance` parsed this. Now startup args are
checked too.

### 3. Guard sync with encryption readiness
`syncNow()` now checks `encryptionService.isReady()` before attempting
encrypt/decrypt. Fresh installs without passphrase setup get a clear
error instead of crashing.

## Test plan

- [x] `pnpm typecheck` passes (17/17)
- [x] `pnpm test` passes (42/42)
- [ ] Manual: change passphrase → verify recovery key still works
- [ ] Manual: close app → click magic link on Windows → app launches and
authenticates
- [ ] Manual: fresh install without passphrase → sync attempt shows
clear error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Bug Fixes
* Deep link authentication tokens passed as command-line arguments are
now properly recognized and processed on application startup
* Sync operations are prevented from proceeding when the encryption
service is not initialized, ensuring system stability
* Recovery-wrapped keys are now properly preserved during sync
operations instead of being inadvertently cleared

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant