feat: Phase 9 — Tauri desktop client with FUSE mount#63
Conversation
Phase 09: Desktop Client - Implementation decisions documented - Phase boundary established Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedUse the following commands to manage reviews:
WalkthroughAdds a Tauri desktop client (TypeScript + Rust) with desktop-specific auth branching (body refreshToken), macOS Keychain token storage, a Rust crypto library (AES/ECIES/Ed25519/IPNS), an inline IPNS parser, a FUSE-backed encrypted vault and background sync/tray, tokenPrefix DB optimization and migration, OpenAPI/model updates, and many tests/configs. Changes
Sequence Diagram(s)sequenceDiagram
participant WebView as Desktop Webview
participant Tauri as Tauri Runtime
participant RustBackend as Rust IPC Handler
participant Keychain as macOS Keychain
participant API as CipherBox API
participant IPFS as IPFS/IPNS
WebView->>Tauri: handle_auth_complete(idToken, privateKey)
Tauri->>RustBackend: invoke handle_auth_complete
RustBackend->>API: POST /auth/login (idToken, publicKey) [X-Client-Type: desktop]
API-->>RustBackend: accessToken + refreshToken (in body)
RustBackend->>Keychain: store_refresh_token(user_id, refreshToken)
RustBackend->>API: GET /vault (Authorization: Bearer accessToken)
API-->>RustBackend: encrypted vault metadata (CID)
RustBackend->>IPFS: GET /ipfs/{cid}
IPFS-->>RustBackend: encrypted metadata bytes
RustBackend->>RustBackend: decrypt metadata, populate inodes, mount FUSE
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
Phase 9: Desktop Client - Standard stack identified (Tauri v2, fuser, FUSE-T, keyring) - Architecture patterns documented (Rust FUSE + crypto bridge) - Pitfalls catalogued (FUSE-T limitations, cookie auth, deep link dev) - Open questions flagged (fuser+FUSE-T compat, eciesjs format) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 09: Desktop Client - 6 plans in 4 waves - Wave 1: scaffold + crypto (09-01) || API auth changes (09-02) [parallel] - Wave 2: desktop auth flow (09-03) - Wave 3: FUSE read ops (09-04), FUSE write ops (09-05) [sequential] - Wave 4: system tray + sync daemon (09-06) with human verification checkpoint - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Split 09-01 into scaffold (01) + crypto (new 02), renumber 02-06 to 03-07 - Add IPNS record creation to crypto plan with cross-language test vectors - Clarify Web3Auth runs in webview (not system browser) for secure key transfer - Add explicit read-only note for open() in FUSE read plan - Split FUSE write Task 2 into file mutations + directory mutations - Add WriteQueue unit tests and offline verification to final plan - Fix wave numbers: FUSE write=4, tray/sync=5 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address 4 remaining issues from plan verification: - Add ipnsPrivateKeyEncrypted to FolderEntry and encryptionMode to FileEntry with camelCase Serde (plans 02, 05, 06) - Add encryptedRootIpnsPrivateKey/rootIpnsPublicKey to VaultResponse and AppState (plan 04) - Fix wave numbering cascade: 04→w3, 05→w4, 06→w5, 07→w6 - Update CONTEXT.md to reflect webview-based Web3Auth decision Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cross-language data model fidelity when porting TypeScript types to Rust — encrypted key fields are the most dangerous to miss since reads work fine but every write path breaks silently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect X-Client-Type: desktop header in login, refresh, logout - Desktop login returns refreshToken in response body (no cookie) - Desktop refresh reads refreshToken from body instead of cookie - Desktop logout skips cookie clearing - Add DesktopRefreshDto for body-based refresh token - Add optional refreshToken to LoginResponseDto and TokenResponseDto - Update existing tests for new method signatures - Regenerate OpenAPI spec and API client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Login with desktop header: verify refreshToken in body, no cookie - Refresh with desktop header: verify body-based token, no cookie - Refresh with desktop header + no token: verify UnauthorizedException - Logout with desktop header: verify clearCookie not called - Logout with desktop header: verify authService.logout still called - All existing web flow tests continue to pass as regression coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create apps/desktop as pnpm workspace member (@cipherbox/desktop) - Add Cargo.toml with all Rust dependencies (tauri, keyring, reqwest, aes-gcm, ecies, ed25519-dalek, tokio, etc.) - Configure tauri.conf.json: no default window, deep-link scheme "cipherbox", ActivationPolicy::Accessory - Register plugins: deep-link, autostart (LaunchAgent), shell, notification - Make fuser optional (requires FUSE-T installed) -- enabled via "fuse" cargo feature in plan 09-03 - Add placeholder icons, build.rs, capabilities, minimal index.html + main.ts webview shell - cargo check passes, package visible in pnpm workspace Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Add desktop client support to auth endpoints - Update auth controller tests for desktop client flow SUMMARY: .planning/phases/09-desktop-client/09-03-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 1/1 - Scaffold Tauri v2 app in monorepo SUMMARY: .planning/phases/09-desktop-client/09-01-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…etadata - AES-256-GCM encrypt/decrypt/seal/unseal matching TypeScript sealed format - ECIES wrap/unwrap using ecies crate (cross-compatible with eciesjs) - Ed25519 keygen/sign/verify with deterministic signatures via ed25519-dalek - FolderMetadata/FolderEntry/FileEntry with serde camelCase renaming - FolderEntry includes ipnsPrivateKeyEncrypted, FileEntry includes encryptionMode - IPNS module stubbed for Task 2 implementation - Utils: random generation, hex encoding, zeroize Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- IPNS record creation with CBOR data, V1+V2 Ed25519 signatures - IPNS record marshaling to protobuf (IpnsEntry with all 9 fields) - IPNS name derivation: CIDv1 base36 (k51...) from Ed25519 public key - 51 tests passing including cross-language vectors from TypeScript: - AES-256-GCM: fixed key/IV produces byte-identical ciphertext - Ed25519: deterministic signature matches TypeScript output - ECIES: Rust unwraps TypeScript-wrapped data successfully - IPNS name: identical k51... string as TypeScript deriveIpnsName - Test vector generation script (generate-test-vectors.mjs) committed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Task 1: AES-256-GCM, ECIES, Ed25519, folder metadata structs - Task 2: IPNS record creation and 51 cross-language test vectors SUMMARY: .planning/phases/09-desktop-client/09-02-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ApiClient with X-Client-Type: desktop header on all requests - Add Keychain storage for refresh tokens via keyring crate - Add request/response types matching backend API (camelCase serde) - Add AppState with memory-only sensitive key fields including root IPNS keypair - Register AppState with Tauri in main.rs setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d Web3Auth - Add commands.rs with handle_auth_complete, try_silent_refresh, logout IPC commands - Register commands in main.rs invoke_handler - Add fetch_and_decrypt_vault helper for ECIES vault key decryption including root IPNS keypair - Add JWT sub claim extraction and secp256k1 public key derivation utilities - Create auth.ts webview module with Web3Auth SDK initialization and login/logout - Update main.ts entry point with silent refresh attempt and login UI flow - Add @web3auth/modal dependency to desktop package.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Task 1: Implement API client, Keychain auth, and app state - Task 2: Implement Tauri commands for auth flow with webview-based Web3Auth SUMMARY: .planning/phases/09-desktop-client/09-04-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…layer - Add api/ipfs.rs: fetch_content and upload_content via backend API - Add api/ipns.rs: resolve_ipns with IpnsResolveResponse struct - Add fuse/inode.rs: InodeTable, InodeData, InodeKind (Root/Folder/File) - Root and Folder variants store decrypted ipns_private_key - File variant includes encryption_mode field - populate_folder decrypts both folder_key and ipns_private_key for subfolders - Add fuse/cache.rs: MetadataCache (30s TTL) and ContentCache (256 MiB LRU) - Add fuse/mod.rs: CipherVaultFS struct, mount_filesystem, unmount_filesystem - Add multipart POST support to ApiClient for file uploads - 64 tests passing (8 new cache tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Implement fuser::Filesystem trait for CipherVaultFS (feature-gated): - init: populate root folder from IPNS on mount - lookup: lazy folder loading when children not yet populated - getattr: return cached FileAttr from inode table - readdir: ALL entries in single pass (FUSE-T requirement) - open: read-only, returns EACCES for write flags (deferred to 09-06) - read: fetch encrypted from IPFS -> ECIES unwrap key -> AES decrypt -> cache - release: remove file handle (dirty handling deferred to 09-06) - statfs: report 500 MiB quota with used blocks from known file sizes - access: check inode existence - Wire mount_filesystem after auth completion in handle_auth_complete - Wire unmount_filesystem before key clearing in logout - Async pattern: tokio runtime for IPFS/IPNS fetches, never block FUSE thread - Background metadata refresh on stale readdir (fire-and-forget) - File key zeroed from memory after decryption Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - IPFS/IPNS API calls, inode table, and caching layer - FUSE read operations (Filesystem trait) and mount/unmount wiring SUMMARY: .planning/phases/09-desktop-client/09-05-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #63 +/- ##
==========================================
+ Coverage 89.57% 89.68% +0.10%
==========================================
Files 42 43 +1
Lines 1429 1483 +54
Branches 268 284 +16
==========================================
+ Hits 1280 1330 +50
- Misses 76 77 +1
- Partials 73 76 +3
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
- Add OpenFileHandle with temp file write buffering (file_handle.rs) - Add IPFS upload_content and unpin_content API functions - Add IPNS publish_ipns with IpnsPublishRequest matching backend DTO - Add update_folder_metadata helper: rebuilds metadata, encrypts, uploads to IPFS, creates/signs IPNS record, publishes via API - Add temp_dir, public_key, tee_public_key fields to CipherVaultFS - Update mount_filesystem signature with new parameters - Update commands.rs to pass public_key and TEE keys to FUSE mount - Clean up temp directory on unmount Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- create(): allocate inode, create temp file, add to parent children - write(): buffer to temp file at offset, mark dirty, update inode size - open(): handle O_WRONLY/O_RDWR with temp file pre-populated from IPFS - release(): if dirty, encrypt content, wrap key (ECIES), upload to IPFS, update inode metadata, publish folder metadata via IPNS - unlink(): remove inode, fire-and-forget unpin, update parent metadata - setattr(): handle truncate via size parameter (FUSE-T compatible) - flush(): no-op (upload happens on release, not flush) - read(): handle both temp-file reads and IPFS-cached reads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- mkdir(): generate Ed25519 IPNS keypair, derive IPNS name, create empty folder metadata, encrypt and upload to IPFS, create+sign+publish IPNS record, enroll TEE for republishing, store decrypted ipns_private_key in new folder's inode, update parent metadata - rmdir(): verify ENOTEMPTY, remove inode, update parent metadata, fire-and-forget unpin of folder's IPNS CID - rename(): handle same-folder rename and cross-folder move, replace existing destination if present, update both parents' metadata for cross-folder moves using per-folder IPNS private keys - Make InodeTable.name_to_ino public for rename index manipulation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 3/3 - Temp-file write model and IPNS publish helpers - FUSE file mutation operations (create, write, release, unlink, setattr, flush) - FUSE directory mutation operations (mkdir, rmdir, rename) SUMMARY: .planning/phases/09-desktop-client/09-06-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ne write queue - Add tray/status.rs: TrayStatus state machine (NotConnected, Mounting, Syncing, Synced, Offline, Error) - Add tray/mod.rs: Menu bar icon with status, Open CipherVault, Sync Now, Login, Logout, Quit items - Add sync/mod.rs: SyncDaemon with 30s IPNS polling, sequence number change detection, offline handling - Add sync/queue.rs: WriteQueue with FIFO processing, retry logic, UploadHandler trait for testability - Add sync/tests.rs: 7 unit tests covering enqueue, process, FIFO order, retry, max-retries drop - Update main.rs: Add mod tray/sync, build tray in setup, register start_sync_daemon command - Update commands.rs: Add start_sync_daemon IPC command, tray status updates in auth/mount lifecycle - Update state.rs: Add sync_trigger channel for tray menu Sync Now button Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
beforeDevCommand was empty so Tauri waited for a frontend server that never started. Added vite.config.ts, vite/typescript dev deps, and wired up beforeDevCommand/beforeBuildCommand in tauri.conf.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The app starts headless (no windows) so clicking Login in the tray menu needs to create a WebviewWindow, not just show a non-existent "main" window. Creates a 480x600 centered window loading index.html. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Web3Auth SDK depends on Node.js globals (Buffer, process) that don't exist in browser/webview environments. Added polyfills.ts (same as web app), Vite resolve aliases, and buffer/process dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Web3Auth v10 renamed initModal() to init() and changed to static imports instead of dynamic. Updated auth.ts to use proper v10 API with typed Web3AuthOptions, WEB3AUTH_NETWORK enum, and WALLET_CONNECTORS constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tauri v2 blocks window.open() by default, which prevents Web3Auth Google OAuth popups. Added on_new_window handler returning Allow to permit authentication popup windows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NewWindowResponse::Allow doesn't maintain window.opener on macOS, breaking Web3Auth's postMessage callback after Google OAuth completes. Using NewWindowResponse::Create with window_features() passes the opener's WKWebViewConfiguration, preserving the window.opener reference. Also added Cross-Origin-Opener-Policy header to Vite dev server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Web3Auth v10 removed authenticateUser() and replaced it with
getIdentityToken() which returns { idToken: string }. Also fixed
provider.request type parameters to match v10 generics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Web3Auth may auto-connect from cached session on init, but the Rust side has no keys in memory on cold start. Now disconnects/clears cache if already connected after init. Added console logging throughout the login flow to help diagnose issues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The backend login endpoint requires publicKey (hex-encoded secp256k1
uncompressed public key) and loginType ("social") fields. Derive
public key from private key before making the login call.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Web3Auth JWT stores the compressed secp256k1 public key (33 bytes). The backend does an exact string match, so we must send compressed format, not uncompressed (65 bytes). Uncompressed key is still used for ECIES operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add vault initialization flow for new users (POST /vault/init) with client-side key generation, ECIES wrapping, and IPNS name derivation - Add window capability permissions (hide/show/focus/close) so the login window can hide after successful auth - Intercept close_requested on main window to hide instead of quitting, keeping the app running as a menu-bar utility - Update tray to Synced after auth when FUSE feature is disabled - Rename mount point and FUSE struct from CipherVault to CipherBox Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eanup 16 fixes from UAT testing (14/15 pass, 1 known issue): FUSE-T NFS: - Force unmount fallback (diskutil unmount force) - Inode reuse in populate_folder (prevents stale file handle) - Deduplicate readdir refresh (only on offset=0) - Parent mtime bump on children change - Eager subfolder pre-population during mount - Stale mount point cleanup on startup Login/logout lifecycle: - Webview reload on re-login (reset stale DOM) - OAuth popup cleanup after auth completes - Web3Auth clearCache instead of destructive logout - Keychain delete-before-set for token persistence Cleanup: - Remove all eprintln diagnostic lines (errors → log::warn) - FUSE-T userspace build config (.cargo/config.toml, pkg-config/fuse.pc) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four learning documents from desktop client development: - FUSE-T NFS gotchas (single-thread, inode stability, READDIR caching) - Tauri webview lifecycle (window reuse, Web3Auth clearCache) - macOS system integration (keychain, force unmount, Spotlight) - Desktop testing strategy (auth bypass proposal for automated testing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove path exclusions for .learnings/, .planning/, and *.lock - Add path instructions to use markdown/planning/learnings for context but skip formatting and linting comments (handled by markdownlint) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Revert `new Function` dynamic import back to `await import('ipns')`
which works with jest module mocking (the Function constructor bypass
was only needed for the desktop Tauri build, not the API)
- Restore re-throw behavior in resolveRecord when DB cache is empty
after a routing error (preserves existing test expectations)
- Remove unused generateEd25519Keypair import
- Fix prettier formatting for long hex strings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ipns npm package is ESM-only, forcing a dynamic import() hack that broke Jest mocking and masked a behavioral issue where resolveRecord would re-throw BAD_GATEWAY when DB cache was empty — hanging the desktop FUSE NFS thread. Replace with ~65 lines of inline protobuf wire format parsing that extracts the two fields we need (value, sequence). resolveRecord now returns null instead of throwing when both routing and DB cache fail, which the FUSE client handles gracefully. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users see "EMPTY DIRECTORY" for 10-30s while IPNS resolves on login, which is alarming. Track initial sync state and show a distinct loading UI until the first resolve completes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement memory zeroization (H-1/H-2/H-3/H-6), FUSE permission enforcement (H-4), secure temp file deletion (H-5), token prefix indexing (M-1), restricted dir permissions (M-2/M-5), eliminate intermediate key copies (M-3), and fix lock discipline (M-4). Key changes: - Wrap all key fields in Zeroizing<Vec<u8>> across CipherBoxFS, InodeKind - Add Drop impl on CachedContent with zeroize, destroy() callback on unmount - Zeroize JSON intermediates in encrypt/decrypt_folder_metadata - Implement owner-only access() permission checking - Zero-overwrite temp files before deletion, restrict to 0o600/0o700 - Add tokenPrefix column for O(1) refresh token lookup (migration included) - Symlink check on mount point, consolidated clear_keys() lock discipline Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only build crypto, api, and web packages — the desktop Tauri/Rust build is unnecessary for browser e2e tests and adds significant time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The rotateRefreshToken query now filters by tokenPrefix for O(1) lookup — update test assertion to expect the new field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover all branches: valid records, large sequence numbers, missing fields, unknown wire types (varint, length-delimited, fixed32, fixed64), truncated varints, oversized varints, and buffer overflows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Tauri/Rust build requires system libraries (glib-2.0, etc.) not available on the Ubuntu runner. Only build crypto, api, and web. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
~/CipherVaultcipherbox://) callbackScope
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Documentation