Feat/tauri desktop v2 - #245
Merged
Merged
Conversation
Old effort archived at tag archive/tauri-desktop-2026-07. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exclusion build script moves API routes, middleware, and dynamic marketing segments aside, builds with TAURI_BUILD=1, restores failure-safe. Gates cookies()/headers() in app layout and i18n request config behind TAURI_BUILD. Produces apps/web/out/ with all 57 tool pages. Web build path unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/desktop: Tauri v2 scaffold with single local_api(method,path,body) command dispatching to a Rust router that mirrors FastAPI /api/v1/* contracts. SQLCipher DB keyed by a Keychain device key; generic encrypted-envelope entries schema with sync metadata (dirty flag, tombstones, last_synced_at). Local handlers: master-vault, backup codes, synthetic personal workspace; loud 501 stubs for unmapped routes. 5 Rust router tests. apps/web: desktop seam in lib/desktop/ (isDesktop guard, invoke bridge, apiFetch) with branches at the backend-auth choke points (proxyJsonAuthed, backendFetch, session helpers) and a synthetic local user in useAuth. All branches compile out of web bundles via NEXT_PUBLIC_TAURI env guard. Verified: tauri dev boots, webview loads, local_api round-trips (user-preferences 501 logged as expected — Phase 3 route). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rust handlers mirroring FastAPI contracts for: password-manager, api-keys, environment-manager (envelope entries), sql/redis/nosql connection vaults, code-snippets, bookmarks + folders (snapshot, move, import, clear-all), notes (ISO timestamps, recursive delete), tasks + projects (wrapped list, filters, stats, export/import), api-client (collections with items:apply-delta tree ops, environments, history, workspaces, public-mocks), user-preferences (singleton with backend-synced defaults, tool-usage, nosql query history). Remote-only prefixes (url-shortener, s3-drive, game-scores, dns-lookup) rejected with 503 until the Phase 4 remote bridge. Query strings now flow through the bridge to handlers. 15 router tests pass; tauri dev boots with zero unmapped-route hits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gating Rust remote_api command (reqwest + persistent cookie jar serialized into SQLCipher kv) calls FastAPI directly: no CORS, HttpOnly JWT cookie flow unchanged, session survives restarts. clear_remote_session wipes the jar on sign-out. Desktop data routing (lib/desktop/router.ts): remote-only tools (url-shortener, s3-drive, game-scores, dns-lookup) always remote with a sign-in gate; workspaces API merges the local personal workspace with remote orgs/workspaces when signed in; a non-local active workspace routes all data calls remote (shared workspaces stay cloud-only, DEK crypto unchanged in the webview); everything else stays local. Cloud sign-in via system browser (OAuth popups don't work in WKWebView): web login page with ?desktop=1 mints a Firebase custom token (new POST /api/v1/auth/desktop-token, same pattern as passkey) and hands it back through the mydevtools:// deep link; the app signs in with signInWithCustomToken and runs the normal session exchange through the Rust jar. DesktopOnlineGate wraps s3-drive, url-shortener, dns-lookup, email-validator; notes image upload requires cloud sign-in; useAuth prefers a real Firebase user over the synthetic local user. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Internal /desktop/sync/* Rust endpoints expose raw row state (dirty rows, tombstones) and resolution actions: mark-synced (with server-id re-key; synced tombstones hard-deleted), apply-remote (clean upsert), remove-local. Toggling sync off clears last_synced_at + sync_state so re-enabling does a fresh additive merge instead of inferring remote deletions. TS engine (lib/desktop/sync-engine.ts) reconciles the local personal workspace against the user's remote personal workspace: push dirty first (tombstone→DELETE, new→POST+re-key, else PATCH; 404-on-update recreates, 409-on-create updates), then pull with LWW on updatedAt — dirty local rows never overwritten; clean+synced rows absent remotely are deleted locally. 13 tool adapters (envelope vaults, DB connections, snippets, bookmarks+folders, notes, tasks+projects, api-client collections/environments/history). Runs on session events, network regain, 5-min interval, and debounced 5s after local writes; rounds skip while a shared workspace is active (workspace cookie would misroute). Settings page gets a desktop-only Cloud Sync card (sign-in, toggle, sync-now). 16 Rust tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/api/sql-client/* (tokio-postgres text protocol + mysql_async — SQL splitter ported from lib/sql-split.ts with quote/comment/dollar-quote awareness), /api/nosql/* (official mongodb driver, clients cached per connection string, node-compatible JSON serialization: ObjectId→hex, Date→ISO, recursive _id conversion, aggregation stage blocklist), and /api/redis-commander/* (redis-rs, per-request connections like the web routes; execute/keys/key CRUD/copy/rename/bulk-delete/flush/export/ import/info/clients/slowlog/scanner/json/search/streams/timeseries/ pubsub-publish; dangerous-command blocklist kept). Localhost hosts allowed — local databases are the point of the desktop app. MONITOR and pub/sub subscribe (SSE on web) return 501, deferred to v1.1. Frontend: 46 raw fetch() call sites across 20 sql-client / nosql-explorer / redis-commander files swept to apiFetch (pass-through fetch on web); live-DB paths always route to the local Rust drivers regardless of active workspace. Verified: 18 unit tests + mongo/redis integration roundtrips against local mongod:27017 and redis:6379 (cargo test -- --ignored). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rust http_request mirrors the /api/proxy envelope exactly (status, statusText, headers, setCookies, redirectChain, body, isBase64, time, size): manual redirect follower with RFC 7231 301/302/303→GET semantics, multipart form-data rebuild per hop, textual-vs-base64 body detection. Desktop allows localhost/private targets (user's own machine); cloud metadata endpoints stay blocked. http_request_stream relays SSE chunks over a tauri Channel with cancel support; sse-client gets a desktop branch feeding the same event parser. apiFetch routes /api/proxy → http_request and returns clear 501s for proxy-grpc/ntlm/spnego/mock (v1.1). 9 more proxy call sites swept (api-client, recorder, fuzz, graphql-introspect, oauth2, scim, runner, perf runner, grpc-web). gitignore-generator works fully offline: all 569 toptal templates bundled into public/gitignore-templates.json (459KB), desktop branch reads the bundle instead of the proxy route. Verified: httpbin parity test (GET headers/args, POST, 302-flips-POST- to-GET with redirectChain, binary→base64, metadata 403). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add tauri-plugin-window-state (window size/position persist). Fix beforeBuildCommand path (runs from apps/desktop). build:desktop now bundles the .app then wraps it with scripts/make-dmg.sh (hdiutil) — Tauri's bundle_dmg.sh drives Finder via AppleScript and flakes in restricted sessions; hdiutil is deterministic and needs no Finder. Adds apps/desktop/README.md (architecture, dev workflow, Gatekeeper right-click-open, regression matrix, v1.1 deferrals). Verified: full release build → 25MB unsigned MyDevTools.dmg; the built .app launches, creates the SQLCipher DB (Keychain device key), and the DB is unreadable by plain sqlite3 (encryption confirmed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Desktop now boots to the login screen instead of the synthetic-local dashboard. Root / gate: on desktop, probe the cloud session (remote_api → backend) and route to /dashboard if authed, else /login — the marketing landing never renders in the app. New DesktopLogin panel (rendered by LoginForm when isDesktop, since OAuth popups don't work in WKWebView): 'Sign in with your browser' opens the system browser at <webview-origin>/login?desktop=1 (dev) or the configured site (packaged); 'Continue without signing in' enters with the local vault. desktopWebBase() reuses the webview origin in dev so the minted token validates against the same backend. On the browser handoff, the deep-link handler establishes the session and fires mydevtools:desktop-authed → DesktopInit routes to /dashboard via Next (static-export paths resolve). LoginRedirectIfAuthed ignores the synthetic desktop-local user so the sign-in screen isn't bypassed. Verified via instrumented boot: desktop webview redirects to /login, remote_api probes backend :8006 session/check (401→refresh→stay on login), local_api serves user-preferences — desktop path confirmed active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Return leg was dead in dev: macOS routes mydevtools:// only to a bundled .app (Info.plist), never the bare 'tauri dev' binary, so the browser callback never reached the running app. Switch to the native-app loopback pattern: await_browser_auth binds an ephemeral 127.0.0.1 port, reports it to the frontend over a Channel, and the browser redirects the minted token to http://127.0.0.1:<port>/callback?token=... The Rust server returns a 'Signed in' page and resolves with the token — works identically in dev and packaged, no URL-scheme registration. startCloudSignIn now awaits the full round-trip, then establishes the session and routes to /dashboard. mydevtools:// deep link kept as a secondary path. Also hide the login page's 'Back to home' link on desktop (no marketing site to return to) via HideOnDesktop. Tests: loopback_callback_roundtrip (favicon probe ignored, real callback returns token + Signed-in page) + token parsing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign-in is now the only path on the desktop login screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser callback never fired when the system browser already had a logged-in session: LoginRedirectIfAuthed bounced the /login?desktop=1&cb=<port> handoff page straight to /dashboard, so the token was never minted and the loopback server waited forever. Extract the handoff into lib/desktop-handoff.ts and run it for BOTH the just-authed (login-form) and already-authed (LoginRedirectIfAuthed) paths — an existing session now mints the token and returns to the app instead of redirecting to the dashboard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The already-authed handoff 401'd: the browser's Firebase client session outlives the backend JWT cookie, and /auth/desktop-token requires the cookie. Call ensureBackendSession(user) before handoffDesktopToken so a stale cookie is re-exchanged first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stalled handoff no longer locks the button for the 5-min timeout — clicking again starts a fresh browser handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Desktop sends long_lived=true on /auth/session; backend issues a 60-day refresh cookie (LONG_LIVED_REFRESH_TOKEN_EXPIRE_DAYS, default 60) instead of the web REFRESH_TOKEN_EXPIRE_DAYS, and persists a refresh_long_lived flag on the user so /refresh preserves the 60-day TTL on every rotation. Decoupled from the web session length. The desktop cookie jar (persisted in SQLCipher) keeps the refresh cookie across relaunches, so a returning user stays signed in for up to 60 days of inactivity — sliding, since each refresh resets the TTL. Verified: attach_auth_cookies(refresh_days=60) emits Max-Age=5184000. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sync Phase A — offline-first entry: desktop launch now always routes to /dashboard (the local master-password gate is the real security boundary); the cloud session probe is fire-and-forget and never routes. Fixes the /login dead-end offline and stops kicking out signed-in users who lose internet. /login stays reachable for the optional browser sign-in. Phase B — stop third-party leaks that bypassed the local store; route through the Rust proxy (http_request), online-only, no data upload: - proxyGet() helper + useFaviconSrc hook + FaviconImg component; bookmark + password-entry favicons fetch via proxy → data URL, letter avatar fallback offline (no more Google/DuckDuckGo hits from webview). - HIBP breach check routes through proxy (k-anonymity prefix only); button disabled offline. - Notes image upload blocked unless Cloud Sync is on + signed in. - OpenAPI-by-URL import routes through proxy, skipped offline. Phase C — preferences sync: syncPreferences() in the sync round (container-level LWW on updatedAt) syncs theme/enabledTools/favorites/ pinned/toolStats to cloud when sync is on. Unconfigured local prefs use updatedAt=0 so the cloud copy wins until the user changes one. All desktop-only via isDesktop(); web bundle unaffected. 22 Rust tests pass; export build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.