From fa8ce5c7beb4617a41af5e24df56c84d8bed4fba Mon Sep 17 00:00:00 2001 From: blankll Date: Thu, 17 Sep 2026 00:34:55 +0800 Subject: [PATCH] fix(deep-link): deliver cold-start auth links via pending state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sqlkit://auth link that launches the app emitted the parsed payload during setup — long before the webview's listeners existed, and Tauri events are not queued, so the token was silently dropped. Park the payload in a PendingAuthState instead: the cold-start branch writes it, the running-instance listener double-writes it (covering the window before frontend listeners exist) and still emits, and the frontend pulls it via consume_pending_auth after registering its listeners. consume() takes-and-clears so a delivered token can never be replayed. --- CHANGELOG.md | 4 ++++ src-tauri/src/lib.rs | 39 ++++++++++++++++++++++++++++++++++----- src/App.vue | 37 +++++++++++++++++++++++++++---------- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f75a3ae..212b3c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Entitlements & version-lock subscription model (geekfun#56)** — client-side implementation of the Ultimate entitlement contract: the two server-computed fields `ultimateExpiresAt` + `versionLockHorizon` are consumed via a new Rust entitlement module (persisted cache, offline tolerance, failure degradation, 5-minute refresh throttle, `app.releaseDate <= versionLockHorizon` unlock check). Rust command gates return `ENTITLEMENT_REQUIRED` for AI (agent loop/step, compaction, LLM validation), the whole Transfer module (import/export/migration/structure execution), SSH tunnel establishment, and the MCP bridge (config/policy save + auto-start). The frontend adds an entitlement store, Geekfun login entry, paid-feature gates with upgrade guidance on Data Studio, Transfer, AI assistant sidebar, ER diagram actions, AI/MCP settings, SSH tunnel option in the connection form, plus an Account & Plan settings tab showing the version-lock state with Geekfun login and logout (logout clears the local entitlement cache so entitlements never outlive the account session). +### Fixed + +- **Deep-link sign-in on cold start** — a `sqlkit://auth` link that launches the app no longer drops the token: the payload is parked in a pending-auth slot during startup and the frontend pulls it via `consume_pending_auth` once mounted; running-instance links double-write the same slot, closing the startup window too. + ## [0.8.7] - 2026-08-17 ### Added diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 304eb0b..69fce99 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -82,6 +82,28 @@ fn parse_auth_from_url(url: &str) -> Option { }) } +/// Deep links that arrive before the frontend has mounted cannot be delivered +/// via events (Tauri events are not queued) — they are parked here and the +/// frontend pulls them via `consume_pending_auth` once its listeners are up. +#[derive(Default)] +struct PendingAuthState(std::sync::Mutex>); + +impl PendingAuthState { + fn store(&self, payload: AuthPayload) { + *self.0.lock().unwrap_or_else(|e| e.into_inner()) = Some(payload); + } + + /// Take-and-clear so a delivered token can never be replayed. + fn consume(&self) -> Option { + self.0.lock().unwrap_or_else(|e| e.into_inner()).take() + } +} + +#[tauri::command] +fn consume_pending_auth(state: tauri::State<'_, PendingAuthState>) -> Option { + state.consume() +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { use crate::connection::guardian::ConnectionGuardian; @@ -195,25 +217,31 @@ pub fn run() { use tauri::{Emitter, Listener}; - // Handle deep links received while the app is already running + app.manage(PendingAuthState::default()); + + // Handle deep links received while the app is already running. + // Double-write: the event reaches a loaded frontend, the pending + // slot covers the window before its listeners exist. let app_handle = app.handle().clone(); app.listen("deep-link://new-url", move |event: tauri::Event| { if let Ok(urls) = serde_json::from_str::>(event.payload()) { for url in &urls { if let Some(payload) = parse_auth_from_url(url) { - let _ = app_handle.emit("sqlkit://auth", payload.clone()); + app_handle.state::().store(payload.clone()); + let _ = app_handle.emit("sqlkit://auth", payload); } } } }); - // Handle deep links passed at launch (cold start) + // Cold start: the URL arrived via argv before any frontend + // listener could exist — park it for the pull above. + let pending = app.state::(); use tauri_plugin_deep_link::DeepLinkExt; if let Ok(Some(urls)) = app.deep_link().get_current() { - let app_handle = app.handle().clone(); for url in &urls { if let Some(payload) = parse_auth_from_url(url.as_str()) { - let _ = app_handle.emit("sqlkit://auth", payload); + pending.store(payload); } } } @@ -345,6 +373,7 @@ pub fn run() { commands::generate_ddl_for_objects, commands::execute_sql_content, commands::get_app_version, + crate::consume_pending_auth, crate::entitlement::refresh_entitlement, crate::entitlement::get_entitlement, crate::entitlement::clear_entitlement, diff --git a/src/App.vue b/src/App.vue index 763b137..d50185b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,6 @@