From d5fddf56110504a16e6a444194276f5bc227be41 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Mon, 22 Jun 2026 22:51:38 -0700 Subject: [PATCH] Use console connect flow for desktop auth --- apps/desktop/src-tauri/src/auth.rs | 104 ++++++++++++++++++++++++++--- apps/web/app/(app)/page.tsx | 21 ------ apps/web/app/(auth)/login/page.tsx | 34 ++-------- apps/web/app/auth/connect/page.tsx | 17 +++++ 4 files changed, 118 insertions(+), 58 deletions(-) diff --git a/apps/desktop/src-tauri/src/auth.rs b/apps/desktop/src-tauri/src/auth.rs index 3ae04764..0a05ad24 100644 --- a/apps/desktop/src-tauri/src/auth.rs +++ b/apps/desktop/src-tauri/src/auth.rs @@ -9,7 +9,8 @@ use std::{ const KEYCHAIN_SERVICE: &str = "ai.supermemory.desktop"; const KEYCHAIN_USER: &str = "supermemory-api-token"; const KEYCHAIN_API_URL_USER: &str = "supermemory-api-url"; -const DEFAULT_WEB_URL: &str = "https://app.supermemory.ai"; +const DEFAULT_WEB_URL: &str = "https://console.supermemory.ai"; +const DEFAULT_BROWSER_API_URL: &str = "https://api.supermemory.ai"; #[cfg(debug_assertions)] const DEFAULT_API_URL: &str = "http://localhost:8787"; @@ -121,6 +122,18 @@ pub fn web_url() -> String { std::env::var("SUPERMEMORY_DESKTOP_WEB_URL").unwrap_or_else(|_| DEFAULT_WEB_URL.to_string()) } +fn browser_auth_api_url() -> String { + if let Ok(api_url) = std::env::var("SUPERMEMORY_DESKTOP_API_URL") { + return api_url; + } + + if std::env::var("SUPERMEMORY_DESKTOP_WEB_URL").is_ok() { + return configured_api_url(); + } + + DEFAULT_BROWSER_API_URL.to_string() +} + pub fn store_token(token: String) -> Result<(), String> { store_token_with_api_url(token, Some(configured_api_url())) } @@ -217,10 +230,20 @@ pub fn handle_deep_link(url: &str) -> Result { .iter() .find_map(|(key, value)| (key == "state").then(|| value.to_string())) .ok_or_else(|| "Auth callback did not include state".to_string())?; - let token = params + let api_key = params .iter() - .find_map(|(key, value)| (key == "token").then(|| value.to_string())) - .ok_or_else(|| "Auth callback did not include token".to_string())?; + .find_map(|(key, value)| (key == "apikey").then(|| value.to_string())) + .or_else(|| { + params + .iter() + .find_map(|(key, value)| (key == "apiKey").then(|| value.to_string())) + }) + .or_else(|| { + params + .iter() + .find_map(|(key, value)| (key == "token").then(|| value.to_string())) + }) + .ok_or_else(|| "Auth callback did not include API key".to_string())?; let callback_api_url = params .iter() .find_map(|(key, value)| (key == "apiUrl").then(|| value.to_string())) @@ -231,7 +254,10 @@ pub fn handle_deep_link(url: &str) -> Result { }); verify_browser_state(&state)?; - store_token_with_api_url(token, callback_api_url)?; + store_token_with_api_url( + api_key, + callback_api_url.or_else(|| Some(browser_auth_api_url())), + )?; Ok(AuthChangedEvent { authenticated: true, @@ -297,10 +323,19 @@ fn build_browser_login_url(state: &str) -> Result { let mut url = url::Url::parse(&base) .or_else(|_| url::Url::parse(&format!("{}/", base.trim_end_matches('/')))) .map_err(|error| format!("Invalid Supermemory web URL: {error}"))?; - url.set_path("login"); + url.set_path("auth/connect"); + + let mut callback = url::Url::parse("supermemory://auth-callback") + .map_err(|error| format!("Invalid desktop callback URL: {error}"))?; + callback + .query_pairs_mut() + .append_pair("state", state) + .append_pair("api_url", &browser_auth_api_url()); + url.query_pairs_mut() - .append_pair("desktop-auth", "1") - .append_pair("state", state); + .append_pair("callback", callback.as_str()) + .append_pair("client", "desktop") + .append_pair("name", "Supermemory Desktop"); Ok(url.to_string()) } @@ -367,3 +402,56 @@ fn first_string(value: &Value, paths: &[&[&str]]) -> Option { found.as_str().map(ToString::to_string) }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn browser_auth_url_uses_console_connect_flow() { + std::env::remove_var("SUPERMEMORY_DESKTOP_WEB_URL"); + std::env::remove_var("SUPERMEMORY_DESKTOP_API_URL"); + + let url = url::Url::parse(&build_browser_login_url("state-123").unwrap()).unwrap(); + assert_eq!( + url.as_str().split('?').next().unwrap(), + "https://console.supermemory.ai/auth/connect" + ); + assert_eq!( + url.query_pairs() + .find(|(key, _)| key == "client") + .unwrap() + .1, + "desktop" + ); + assert_eq!( + url.query_pairs().find(|(key, _)| key == "name").unwrap().1, + "Supermemory Desktop" + ); + + let callback = url + .query_pairs() + .find_map(|(key, value)| (key == "callback").then(|| value.to_string())) + .unwrap(); + let callback = url::Url::parse(&callback).unwrap(); + + assert_eq!(callback.scheme(), "supermemory"); + assert_eq!(callback.host_str(), Some("auth-callback")); + assert_eq!( + callback + .query_pairs() + .find(|(key, _)| key == "state") + .unwrap() + .1, + "state-123" + ); + assert_eq!( + callback + .query_pairs() + .find(|(key, _)| key == "api_url") + .unwrap() + .1, + DEFAULT_BROWSER_API_URL + ); + } +} diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index fb4be08a..020cad49 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -149,27 +149,6 @@ export default function NewPage() { } }, [user, session]) - // Desktop auth: hand the browser session token back to the native app via deep link. - useEffect(() => { - const url = new URL(window.location.href) - if (!url.searchParams.get("desktop-auth")) return - const state = url.searchParams.get("state") - const sessionToken = session?.token - if (state && sessionToken) { - const callback = new URL("supermemory://auth-callback") - callback.searchParams.set("token", sessionToken) - callback.searchParams.set("state", state) - callback.searchParams.set( - "apiUrl", - process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai", - ) - window.location.assign(callback.toString()) - url.searchParams.delete("desktop-auth") - url.searchParams.delete("state") - window.history.replaceState({}, "", url.toString()) - } - }, [session]) - // URL-driven modal states const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) const [isSearchOpen, setIsSearchOpen] = useQueryState("search", searchParam) diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index b979fb34..c7723724 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -126,9 +126,6 @@ export default function LoginPage() { const { data: sessionData, isPending: sessionPending } = useSession() const oauthQueryForResume = params.toString() - const desktopAuthState = params.get("desktop-auth") - ? params.get("state") - : null const isRedirecting = !sessionPending && Boolean(sessionData?.session) const isAuthResolving = sessionPending || isRedirecting const loadingMessage = isAuthResolving @@ -151,32 +148,16 @@ export default function LoginPage() { } const redirectUrl = params.get("redirect") if (redirectUrl) { - const dest = resolveAuthRedirectUrl(redirectUrl, window.location.origin) - if (desktopAuthState) { - dest.searchParams.set("desktop-auth", "1") - dest.searchParams.set("state", desktopAuthState) - } - window.location.assign(dest.toString()) - return - } - if (desktopAuthState) { - const dest = new URL("/", window.location.origin) - dest.searchParams.set("desktop-auth", "1") - dest.searchParams.set("state", desktopAuthState) - window.location.assign(dest.toString()) + window.location.assign( + resolveAuthRedirectUrl(redirectUrl, window.location.origin).toString(), + ) return } // Carry the flag so the dashboard posts the session token to the extension (else: sign-in loop). const dest = new URL("/", window.location.origin) dest.searchParams.set("extension-auth-success", "true") window.location.assign(dest.toString()) - }, [ - sessionPending, - sessionData?.session, - oauthQueryForResume, - params, - desktopAuthState, - ]) + }, [sessionPending, sessionData?.session, oauthQueryForResume, params]) // Get redirect URL from query params const redirectUrl = params.get("redirect") @@ -191,12 +172,7 @@ export default function LoginPage() { const finalUrl = resolveAuthRedirectUrl(redirectUrl, origin) - if (desktopAuthState) { - finalUrl.searchParams.set("desktop-auth", "1") - finalUrl.searchParams.set("state", desktopAuthState) - } else { - finalUrl.searchParams.set("extension-auth-success", "true") - } + finalUrl.searchParams.set("extension-auth-success", "true") return finalUrl.toString() } diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 0f96e52c..e5e46ba0 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -18,6 +18,12 @@ const API_URL = function isValidLocalhostCallback(callback: string): boolean { try { const url = new URL(callback) + const isDesktopCallback = + url.protocol === "supermemory:" && + url.hostname === "auth-callback" && + url.searchParams.has("state") + if (isDesktopCallback) return true + const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" const isHttp = url.protocol === "http:" @@ -101,6 +107,17 @@ const PLUGIN_INFO: Record = { ], icon: "/images/plugins/codex.png", }, + desktop: { + name: "Supermemory Desktop", + description: + "Native access to your Supermemory account from the desktop app.", + features: [ + "Secure browser-based sign-in", + "Stores your access key in the system keychain", + "Syncs search, MCP, and local memory features", + ], + icon: "/icon.png", + }, } function getPluginName(client: string): string {