mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
Add desktop browser auth
This commit is contained in:
parent
d20026a28a
commit
9cc762c2e4
9 changed files with 972 additions and 17 deletions
|
|
@ -4,14 +4,61 @@ import { Button } from "@ui/components/button"
|
|||
import { Input } from "@ui/components/input"
|
||||
import { Label } from "@ui/components/label"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { type FormEvent, useState } from "react"
|
||||
import { desktopDevAuthEnabled, storeToken } from "@/lib/auth"
|
||||
import { type FormEvent, useEffect, useState } from "react"
|
||||
import {
|
||||
beginBrowserAuth,
|
||||
desktopDevAuthEnabled,
|
||||
getSession,
|
||||
onAuthChanged,
|
||||
onAuthError,
|
||||
storeToken,
|
||||
} from "@/lib/auth"
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const [token, setToken] = useState("")
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isBrowserAuthPending, setIsBrowserAuthPending] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenChanged: (() => void) | undefined
|
||||
let unlistenError: (() => void) | undefined
|
||||
|
||||
onAuthChanged(async (event) => {
|
||||
if (!event.authenticated) return
|
||||
setError(null)
|
||||
setIsBrowserAuthPending(false)
|
||||
try {
|
||||
await getSession()
|
||||
router.replace("/")
|
||||
} catch (err) {
|
||||
setError(formatError(err, "Could not validate browser sign-in"))
|
||||
}
|
||||
})
|
||||
.then((handler) => {
|
||||
unlistenChanged = handler
|
||||
})
|
||||
.catch(() => {
|
||||
unlistenChanged = undefined
|
||||
})
|
||||
|
||||
onAuthError((message) => {
|
||||
setIsBrowserAuthPending(false)
|
||||
setError(message)
|
||||
})
|
||||
.then((handler) => {
|
||||
unlistenError = handler
|
||||
})
|
||||
.catch(() => {
|
||||
unlistenError = undefined
|
||||
})
|
||||
|
||||
return () => {
|
||||
unlistenChanged?.()
|
||||
unlistenError?.()
|
||||
}
|
||||
}, [router])
|
||||
|
||||
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
|
@ -34,6 +81,17 @@ export default function LoginPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function startBrowserAuth() {
|
||||
setError(null)
|
||||
setIsBrowserAuthPending(true)
|
||||
try {
|
||||
await beginBrowserAuth()
|
||||
} catch (err) {
|
||||
setError(formatError(err, "Could not open browser sign-in"))
|
||||
setIsBrowserAuthPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
{/* Keep the frameless window draggable from the top edge. */}
|
||||
|
|
@ -44,12 +102,23 @@ export default function LoginPage() {
|
|||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
Sign in to access your memories.
|
||||
</p>
|
||||
<Button className="mt-6 w-full" disabled>
|
||||
Sign in with browser
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-6 w-full"
|
||||
disabled={isBrowserAuthPending || isSubmitting}
|
||||
onClick={startBrowserAuth}
|
||||
>
|
||||
{isBrowserAuthPending
|
||||
? "Waiting for browser..."
|
||||
: "Sign in with browser"}
|
||||
</Button>
|
||||
<p className="mt-3 text-muted-foreground text-xs">
|
||||
Browser-based sign-in arrives in a later phase.
|
||||
We'll open Supermemory in your browser and return here after
|
||||
sign-in.
|
||||
</p>
|
||||
{error ? (
|
||||
<p className="mt-4 text-destructive text-sm">{error}</p>
|
||||
) : null}
|
||||
{desktopDevAuthEnabled ? (
|
||||
<form className="mt-6 space-y-3 text-left" onSubmit={onSubmit}>
|
||||
<div className="space-y-2">
|
||||
|
|
@ -63,9 +132,6 @@ export default function LoginPage() {
|
|||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
) : null}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
|
|
@ -80,3 +146,11 @@ export default function LoginPage() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatError(error: unknown, fallback: string) {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: fallback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { listen } from "@tauri-apps/api/event"
|
||||
|
||||
export type AuthSession = {
|
||||
userId: string
|
||||
|
|
@ -10,6 +11,13 @@ export type AuthSession = {
|
|||
}
|
||||
|
||||
export const desktopDevAuthEnabled = process.env.NEXT_PUBLIC_DESKTOP_DEV === "1"
|
||||
export const AUTH_CHANGED_EVENT = "auth:changed"
|
||||
export const AUTH_ERROR_EVENT = "auth:error"
|
||||
|
||||
export type AuthChangedEvent = {
|
||||
authenticated: boolean
|
||||
apiUrl?: string | null
|
||||
}
|
||||
|
||||
export async function getStoredToken() {
|
||||
return invoke<string | null>("auth_get_token")
|
||||
|
|
@ -24,6 +32,22 @@ export async function storeToken(token: string) {
|
|||
return getSession()
|
||||
}
|
||||
|
||||
export async function beginBrowserAuth() {
|
||||
return invoke<string>("auth_begin_browser")
|
||||
}
|
||||
|
||||
export async function clearSession() {
|
||||
await invoke("auth_clear")
|
||||
}
|
||||
|
||||
export function onAuthChanged(handler: (event: AuthChangedEvent) => void) {
|
||||
return listen<AuthChangedEvent>(AUTH_CHANGED_EVENT, (event) => {
|
||||
handler(event.payload)
|
||||
})
|
||||
}
|
||||
|
||||
export function onAuthError(handler: (message: string) => void) {
|
||||
return listen<string>(AUTH_ERROR_EVENT, (event) => {
|
||||
handler(event.payload)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
530
apps/desktop/src-tauri/Cargo.lock
generated
530
apps/desktop/src-tauri/Cargo.lock
generated
|
|
@ -47,6 +47,137 @@ version = "1.0.102"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
|
|
@ -142,6 +273,19 @@ dependencies = [
|
|||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.4"
|
||||
|
|
@ -337,6 +481,35 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
|
|
@ -420,6 +593,12 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -621,6 +800,15 @@ dependencies = [
|
|||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||
dependencies = [
|
||||
"const-random",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dom_query"
|
||||
version = "0.27.0"
|
||||
|
|
@ -707,6 +895,33 @@ version = "1.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -734,6 +949,27 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
|
|
@ -855,6 +1091,19 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
|
|
@ -1223,6 +1472,12 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
|
|
@ -1241,6 +1496,12 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
|
|
@ -2102,6 +2363,26 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||
dependencies = [
|
||||
"dlv-list",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
|
|
@ -2127,6 +2408,12 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
|
|
@ -2215,6 +2502,17 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
|
|
@ -2260,6 +2558,20 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
|
|
@ -2626,6 +2938,16 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
|
|
@ -2983,6 +3305,16 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
|
|
@ -3117,8 +3449,12 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-single-instance",
|
||||
"toml_edit 0.22.27",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3372,6 +3708,27 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry",
|
||||
"windows-result 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.3.2"
|
||||
|
|
@ -3387,6 +3744,22 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-deep-link",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.11.3"
|
||||
|
|
@ -3487,6 +3860,19 @@ dependencies = [
|
|||
"toml 1.1.2+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.5.0"
|
||||
|
|
@ -3567,6 +3953,15 @@ dependencies = [
|
|||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
|
|
@ -3819,9 +4214,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
|
|
@ -3871,6 +4278,17 @@ version = "1.20.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
|
|
@ -4383,6 +4801,17 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
|
@ -4828,6 +5257,67 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc",
|
||||
"ordered-stream",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 1.0.3",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow 1.0.3",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.52"
|
||||
|
|
@ -4913,3 +5403,43 @@ name = "zmij"
|
|||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 1.0.3",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.118",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -22,3 +22,7 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
toml_edit = "0.22"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
use keyring::{Entry, Error as KeyringError};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::{
|
||||
process::Command,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
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";
|
||||
#[cfg(debug_assertions)]
|
||||
const DEFAULT_API_URL: &str = "http://localhost:8787";
|
||||
|
||||
|
|
@ -12,6 +17,8 @@ const DEFAULT_API_URL: &str = "http://localhost:8787";
|
|||
const DEFAULT_API_URL: &str = "https://api.supermemory.ai";
|
||||
|
||||
static TOKEN_CACHE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||
static API_URL_CACHE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||
static PENDING_BROWSER_STATE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -22,15 +29,35 @@ pub struct AuthSession {
|
|||
pub api_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthChangedEvent {
|
||||
pub authenticated: bool,
|
||||
pub api_url: Option<String>,
|
||||
}
|
||||
|
||||
fn token_entry() -> Result<Entry, String> {
|
||||
Entry::new(KEYCHAIN_SERVICE, KEYCHAIN_USER)
|
||||
.map_err(|error| format!("Could not open keychain entry: {error}"))
|
||||
}
|
||||
|
||||
fn api_url_entry() -> Result<Entry, String> {
|
||||
Entry::new(KEYCHAIN_SERVICE, KEYCHAIN_API_URL_USER)
|
||||
.map_err(|error| format!("Could not open keychain API URL entry: {error}"))
|
||||
}
|
||||
|
||||
fn token_cache() -> &'static Mutex<Option<String>> {
|
||||
TOKEN_CACHE.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
fn api_url_cache() -> &'static Mutex<Option<String>> {
|
||||
API_URL_CACHE.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
fn pending_browser_state() -> &'static Mutex<Option<String>> {
|
||||
PENDING_BROWSER_STATE.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
fn set_cached_token(token: Option<String>) -> Result<(), String> {
|
||||
let mut cached = token_cache()
|
||||
.lock()
|
||||
|
|
@ -46,7 +73,22 @@ fn get_cached_token() -> Result<Option<String>, String> {
|
|||
.map_err(|_| "Could not lock token cache".to_string())
|
||||
}
|
||||
|
||||
pub fn api_url() -> String {
|
||||
fn set_cached_api_url(api_url: Option<String>) -> Result<(), String> {
|
||||
let mut cached = api_url_cache()
|
||||
.lock()
|
||||
.map_err(|_| "Could not lock API URL cache".to_string())?;
|
||||
*cached = api_url;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cached_api_url() -> Result<Option<String>, String> {
|
||||
api_url_cache()
|
||||
.lock()
|
||||
.map(|cached| cached.clone())
|
||||
.map_err(|_| "Could not lock API URL cache".to_string())
|
||||
}
|
||||
|
||||
fn configured_api_url() -> String {
|
||||
if let Ok(api_url) = std::env::var("SUPERMEMORY_DESKTOP_API_URL") {
|
||||
return api_url;
|
||||
}
|
||||
|
|
@ -63,7 +105,27 @@ pub fn api_url() -> String {
|
|||
DEFAULT_API_URL.to_string()
|
||||
}
|
||||
|
||||
pub fn api_url() -> String {
|
||||
if let Ok(api_url) = std::env::var("SUPERMEMORY_DESKTOP_API_URL") {
|
||||
return api_url;
|
||||
}
|
||||
|
||||
if let Ok(Some(api_url)) = get_stored_api_url() {
|
||||
return api_url;
|
||||
}
|
||||
|
||||
configured_api_url()
|
||||
}
|
||||
|
||||
pub fn web_url() -> String {
|
||||
std::env::var("SUPERMEMORY_DESKTOP_WEB_URL").unwrap_or_else(|_| DEFAULT_WEB_URL.to_string())
|
||||
}
|
||||
|
||||
pub fn store_token(token: String) -> Result<(), String> {
|
||||
store_token_with_api_url(token, Some(configured_api_url()))
|
||||
}
|
||||
|
||||
pub fn store_token_with_api_url(token: String, api_url: Option<String>) -> Result<(), String> {
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
return Err("Token cannot be empty".to_string());
|
||||
|
|
@ -73,7 +135,13 @@ pub fn store_token(token: String) -> Result<(), String> {
|
|||
|
||||
token_entry()?
|
||||
.set_password(token)
|
||||
.map_err(|error| format!("Could not save token to keychain: {error}"))
|
||||
.map_err(|error| format!("Could not save token to keychain: {error}"))?;
|
||||
|
||||
if let Some(api_url) = api_url {
|
||||
store_api_url(api_url)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_token() -> Result<Option<String>, String> {
|
||||
|
|
@ -84,13 +152,100 @@ pub fn get_token() -> Result<Option<String>, String> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_stored_api_url() -> Result<Option<String>, String> {
|
||||
match api_url_entry()?.get_password() {
|
||||
Ok(api_url) => Ok(Some(api_url)),
|
||||
Err(KeyringError::NoEntry) => get_cached_api_url(),
|
||||
Err(error) => Err(format!("Could not read API URL from keychain: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn store_api_url(api_url: String) -> Result<(), String> {
|
||||
let api_url = normalize_base_url(&api_url)?;
|
||||
set_cached_api_url(Some(api_url.clone()))?;
|
||||
api_url_entry()?
|
||||
.set_password(&api_url)
|
||||
.map_err(|error| format!("Could not save API URL to keychain: {error}"))
|
||||
}
|
||||
|
||||
pub fn clear_token() -> Result<(), String> {
|
||||
set_cached_token(None)?;
|
||||
set_cached_api_url(None)?;
|
||||
|
||||
match token_entry()?.delete_credential() {
|
||||
Ok(()) | Err(KeyringError::NoEntry) => Ok(()),
|
||||
Err(error) => Err(format!("Could not clear token from keychain: {error}")),
|
||||
}?;
|
||||
|
||||
match api_url_entry()?.delete_credential() {
|
||||
Ok(()) | Err(KeyringError::NoEntry) => Ok(()),
|
||||
Err(error) => Err(format!("Could not clear API URL from keychain: {error}")),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn begin_browser_auth() -> Result<String, String> {
|
||||
let state = uuid::Uuid::new_v4().to_string();
|
||||
{
|
||||
let mut pending = pending_browser_state()
|
||||
.lock()
|
||||
.map_err(|_| "Could not lock browser auth state".to_string())?;
|
||||
*pending = Some(state.clone());
|
||||
}
|
||||
|
||||
let login_url = build_browser_login_url(&state)?;
|
||||
open_system_browser(&login_url)?;
|
||||
Ok(login_url)
|
||||
}
|
||||
|
||||
pub fn handle_deep_link(url: &str) -> Result<AuthChangedEvent, String> {
|
||||
let parsed =
|
||||
url::Url::parse(url).map_err(|error| format!("Invalid auth callback URL: {error}"))?;
|
||||
if parsed.scheme() != "supermemory" {
|
||||
return Err("Ignoring non-supermemory deep link".to_string());
|
||||
}
|
||||
|
||||
let is_auth_callback =
|
||||
parsed.host_str() == Some("auth-callback") || parsed.path() == "/auth-callback";
|
||||
if !is_auth_callback {
|
||||
return Err("Ignoring unsupported supermemory deep link".to_string());
|
||||
}
|
||||
|
||||
let params = parsed.query_pairs().collect::<Vec<_>>();
|
||||
let state = params
|
||||
.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
|
||||
.iter()
|
||||
.find_map(|(key, value)| (key == "token").then(|| value.to_string()))
|
||||
.ok_or_else(|| "Auth callback did not include token".to_string())?;
|
||||
let callback_api_url = params
|
||||
.iter()
|
||||
.find_map(|(key, value)| (key == "apiUrl").then(|| value.to_string()))
|
||||
.or_else(|| {
|
||||
params
|
||||
.iter()
|
||||
.find_map(|(key, value)| (key == "api_url").then(|| value.to_string()))
|
||||
});
|
||||
|
||||
verify_browser_state(&state)?;
|
||||
store_token_with_api_url(token, callback_api_url)?;
|
||||
|
||||
Ok(AuthChangedEvent {
|
||||
authenticated: true,
|
||||
api_url: Some(api_url()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_auth_deep_link(url: &str) -> bool {
|
||||
let Ok(parsed) = url::Url::parse(url) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
parsed.scheme() == "supermemory"
|
||||
&& (parsed.host_str() == Some("auth-callback") || parsed.path() == "/auth-callback")
|
||||
}
|
||||
|
||||
pub async fn whoami() -> Result<AuthSession, String> {
|
||||
|
|
@ -137,6 +292,73 @@ pub async fn whoami() -> Result<AuthSession, String> {
|
|||
})
|
||||
}
|
||||
|
||||
fn build_browser_login_url(state: &str) -> Result<String, String> {
|
||||
let base = web_url();
|
||||
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.query_pairs_mut()
|
||||
.append_pair("desktop-auth", "1")
|
||||
.append_pair("state", state);
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
fn open_system_browser(url: &str) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut command = {
|
||||
let mut command = Command::new("open");
|
||||
command.arg(url);
|
||||
command
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut command = {
|
||||
let mut command = Command::new("cmd");
|
||||
command.args(["/C", "start", "", url]);
|
||||
command
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut command = {
|
||||
let mut command = Command::new("xdg-open");
|
||||
command.arg(url);
|
||||
command
|
||||
};
|
||||
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("Could not open browser: {error}"))
|
||||
}
|
||||
|
||||
fn verify_browser_state(state: &str) -> Result<(), String> {
|
||||
let mut pending = pending_browser_state()
|
||||
.lock()
|
||||
.map_err(|_| "Could not lock browser auth state".to_string())?;
|
||||
|
||||
match pending.as_deref() {
|
||||
Some(expected) if expected == state => {
|
||||
*pending = None;
|
||||
Ok(())
|
||||
}
|
||||
Some(_) => Err("Auth callback state did not match".to_string()),
|
||||
None => Err("No browser auth request is pending".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_base_url(api_url: &str) -> Result<String, String> {
|
||||
let api_url = api_url.trim().trim_end_matches('/').to_string();
|
||||
if api_url.is_empty() {
|
||||
return Err("API URL cannot be empty".to_string());
|
||||
}
|
||||
let parsed = url::Url::parse(&api_url).map_err(|error| format!("Invalid API URL: {error}"))?;
|
||||
match parsed.scheme() {
|
||||
"http" | "https" => Ok(api_url),
|
||||
scheme => Err(format!("Unsupported API URL scheme: {scheme}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn first_string(value: &Value, paths: &[&[&str]]) -> Option<String> {
|
||||
paths.iter().find_map(|path| {
|
||||
let found = path
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ mod tools;
|
|||
mod tray;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::Manager;
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
/// Identity of the native app, surfaced to the webview over IPC.
|
||||
/// `rename_all = "camelCase"` makes the JSON keys match the TypeScript `AppInfo`.
|
||||
|
|
@ -43,6 +44,11 @@ fn auth_clear() -> Result<(), String> {
|
|||
auth::clear_token()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn auth_begin_browser() -> Result<String, String> {
|
||||
auth::begin_browser_auth()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn auth_whoami() -> Result<auth::AuthSession, String> {
|
||||
auth::whoami().await
|
||||
|
|
@ -154,6 +160,13 @@ fn tools_disconnect(tool_id: String) -> Result<tools::ToolConnectResult, String>
|
|||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.show();
|
||||
let _ = main.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.setup(|app| {
|
||||
spotlight::create_window(app)?;
|
||||
|
|
@ -162,6 +175,7 @@ pub fn run() {
|
|||
spotlight::register_shortcut(app, &accelerator);
|
||||
tray::create(app)?;
|
||||
smfs::start_status_poller(app.handle().clone());
|
||||
register_auth_deep_link_handler(app.handle());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
|
|
@ -169,6 +183,7 @@ pub fn run() {
|
|||
auth_store_token,
|
||||
auth_get_token,
|
||||
auth_clear,
|
||||
auth_begin_browser,
|
||||
auth_whoami,
|
||||
spotlight_show,
|
||||
spotlight_hide,
|
||||
|
|
@ -194,3 +209,37 @@ pub fn run() {
|
|||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
fn register_auth_deep_link_handler(app: &tauri::AppHandle) {
|
||||
let app_for_listener = app.clone();
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
for url in event.urls() {
|
||||
handle_auth_deep_link(&app_for_listener, url.as_str());
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
||||
for url in urls {
|
||||
handle_auth_deep_link(app, url.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_auth_deep_link(app: &tauri::AppHandle, url: &str) {
|
||||
if !auth::is_auth_deep_link(url) {
|
||||
return;
|
||||
}
|
||||
|
||||
match auth::handle_deep_link(url) {
|
||||
Ok(event) => {
|
||||
let _ = app.emit("auth:changed", event);
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.show();
|
||||
let _ = main.set_focus();
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = app.emit("auth:error", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,12 @@
|
|||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["supermemory"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,27 @@ 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)
|
||||
|
|
|
|||
|
|
@ -126,6 +126,9 @@ 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
|
||||
|
|
@ -148,16 +151,32 @@ export default function LoginPage() {
|
|||
}
|
||||
const redirectUrl = params.get("redirect")
|
||||
if (redirectUrl) {
|
||||
window.location.assign(
|
||||
resolveAuthRedirectUrl(redirectUrl, window.location.origin).toString(),
|
||||
)
|
||||
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())
|
||||
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])
|
||||
}, [
|
||||
sessionPending,
|
||||
sessionData?.session,
|
||||
oauthQueryForResume,
|
||||
params,
|
||||
desktopAuthState,
|
||||
])
|
||||
|
||||
// Get redirect URL from query params
|
||||
const redirectUrl = params.get("redirect")
|
||||
|
|
@ -172,7 +191,12 @@ export default function LoginPage() {
|
|||
|
||||
const finalUrl = resolveAuthRedirectUrl(redirectUrl, origin)
|
||||
|
||||
finalUrl.searchParams.set("extension-auth-success", "true")
|
||||
if (desktopAuthState) {
|
||||
finalUrl.searchParams.set("desktop-auth", "1")
|
||||
finalUrl.searchParams.set("state", desktopAuthState)
|
||||
} else {
|
||||
finalUrl.searchParams.set("extension-auth-success", "true")
|
||||
}
|
||||
return finalUrl.toString()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue