Use Better Auth desktop handoff

This commit is contained in:
Sreeram Sreedhar 2026-06-23 12:37:04 -07:00
parent 889f71ab40
commit eff084a1f9
2 changed files with 202 additions and 13 deletions

View file

@ -13,7 +13,7 @@ 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://console.supermemory.ai";
const DEFAULT_WEB_URL: &str = "https://app.supermemory.ai";
const DEFAULT_BROWSER_API_URL: &str = "https://api.supermemory.ai";
const BROWSER_AUTH_TIMEOUT: Duration = Duration::from_secs(120);
#[cfg(debug_assertions)]
@ -320,7 +320,7 @@ fn build_browser_login_url(state: &str, callback_port: u16) -> Result<String, St
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("auth/agent-connect");
url.set_path("auth/desktop");
let mut callback = url::Url::parse(&format!("http://127.0.0.1:{callback_port}/callback"))
.map_err(|error| format!("Invalid desktop callback URL: {error}"))?;
@ -339,8 +339,7 @@ fn build_browser_login_url(state: &str, callback_port: u16) -> Result<String, St
.append_pair("hostname", "Supermemory Desktop")
.append_pair("os", std::env::consts::OS)
.append_pair("cwd", &cwd)
.append_pair("cli_version", env!("CARGO_PKG_VERSION"))
.append_pair("client", "desktop");
.append_pair("version", env!("CARGO_PKG_VERSION"));
Ok(url.to_string())
}
@ -590,14 +589,14 @@ mod tests {
use super::*;
#[test]
fn browser_auth_url_uses_console_agent_connect_flow() {
fn browser_auth_url_uses_desktop_login_handoff_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", 49876).unwrap()).unwrap();
assert_eq!(
url.as_str().split('?').next().unwrap(),
"https://console.supermemory.ai/auth/agent-connect"
"https://app.supermemory.ai/auth/desktop"
);
assert_eq!(
url.query_pairs()
@ -606,13 +605,7 @@ mod tests {
.1,
"Supermemory Desktop"
);
assert_eq!(
url.query_pairs()
.find(|(key, _)| key == "client")
.unwrap()
.1,
"desktop"
);
assert!(url.query_pairs().all(|(key, _)| key != "client"));
let callback = url
.query_pairs()

View file

@ -0,0 +1,196 @@
"use client"
import { useAuth } from "@lib/auth-context"
import { useSession } from "@lib/auth"
import { Loader2, XCircle } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useMemo, useRef, useState } from "react"
import { PENDING_CONNECT_URL_KEY } from "@/lib/constants"
const API_URL =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
type Status = "loading" | "creating" | "success" | "error"
function isValidDesktopCallback(callback: string): boolean {
try {
const url = new URL(callback)
const isLoopback =
(url.hostname === "localhost" || url.hostname === "127.0.0.1") &&
url.protocol === "http:" &&
url.pathname === "/callback" &&
url.searchParams.has("state")
if (isLoopback) return true
return (
url.protocol === "supermemory:" &&
url.hostname === "auth-callback" &&
url.searchParams.has("state")
)
} catch {
return false
}
}
function DesktopAuthContent() {
const router = useRouter()
const params = useSearchParams()
const { data: session, isPending } = useSession()
const { org, organizations, isRestoring } = useAuth()
const [status, setStatus] = useState<Status>("loading")
const [error, setError] = useState<string | null>(null)
const hasStarted = useRef(false)
const callback = params.get("callback")
const desktopHostname = params.get("hostname") || "Supermemory Desktop"
const desktopOs = params.get("os") || "desktop"
const desktopCwd = params.get("cwd") || ""
const desktopVersion = params.get("version") || "desktop"
const callbackIsValid = useMemo(
() => (callback ? isValidDesktopCallback(callback) : false),
[callback],
)
const shouldRedirectToOnboarding =
!isPending &&
!isRestoring &&
!!session &&
Array.isArray(organizations) &&
organizations.length === 0
useEffect(() => {
if (isPending || isRestoring) return
if (!session) return
if (organizations === null) return
if (organizations.length > 0) return
try {
sessionStorage.setItem(PENDING_CONNECT_URL_KEY, window.location.href)
} catch (err) {
console.warn("Failed to store pending desktop auth URL", err)
}
router.replace("/onboarding")
}, [isPending, isRestoring, session, organizations, router])
useEffect(() => {
if (isPending || isRestoring || shouldRedirectToOnboarding) return
if (hasStarted.current) return
if (!callback) {
setStatus("error")
setError("Missing desktop callback URL.")
return
}
if (!callbackIsValid) {
setStatus("error")
setError("Invalid desktop callback URL.")
return
}
if (!session || !org) {
setStatus("error")
setError("Your account is not fully set up yet.")
return
}
hasStarted.current = true
const callbackUrl = callback
async function finishDesktopAuth() {
try {
setStatus("creating")
const res = await fetch(`${API_URL}/v3/auth/agent-key`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Supermemory Desktop",
permission: "write",
deviceInfo: {
hostname: desktopHostname,
os: desktopOs,
cwd: desktopCwd,
cliVersion: desktopVersion,
},
}),
})
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as {
message?: string
}
throw new Error(data.message ?? "Failed to create desktop key")
}
const data = (await res.json()) as { key: string }
setStatus("success")
const redirectUrl = new URL(callbackUrl)
redirectUrl.searchParams.set("apikey", data.key)
redirectUrl.searchParams.set("api_url", API_URL)
window.location.href = redirectUrl.toString()
} catch (err) {
setStatus("error")
setError(
err instanceof Error ? err.message : "Failed to finish desktop login",
)
}
}
void finishDesktopAuth()
}, [
callback,
callbackIsValid,
desktopCwd,
desktopHostname,
desktopOs,
desktopVersion,
isPending,
isRestoring,
org,
session,
shouldRedirectToOnboarding,
])
if (status === "error") {
return (
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="flex max-w-sm flex-col items-center gap-4 text-center">
<XCircle className="size-10 text-red-400" />
<div>
<h1 className="font-semibold text-[#FAFAFA] text-lg">
Desktop login failed
</h1>
<p className="mt-1 text-[#737373] text-sm">{error}</p>
</div>
</div>
</div>
)
}
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="flex flex-col items-center gap-3">
<Loader2 className="size-6 animate-spin text-[#4BA0FA]" />
<p className="text-[#737373] text-sm">
{status === "success"
? "Returning to Supermemory Desktop..."
: "Finishing desktop login..."}
</p>
</div>
</div>
)
}
export default function DesktopAuthPage() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-background">
<Loader2 className="size-6 animate-spin text-[#4BA0FA]" />
</div>
}
>
<DesktopAuthContent />
</Suspense>
)
}