diff --git a/apps/web/app/auth/agent-connect/page.tsx b/apps/web/app/auth/agent-connect/page.tsx deleted file mode 100644 index 86838d41..00000000 --- a/apps/web/app/auth/agent-connect/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../connect/page" diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx deleted file mode 100644 index febd2760..00000000 --- a/apps/web/app/auth/connect/page.tsx +++ /dev/null @@ -1,484 +0,0 @@ -"use client" - -import { useAuth } from "@lib/auth-context" -import { useSession } from "@lib/auth" -import { cn } from "@lib/utils" -import { dmSans125ClassName } from "@/lib/fonts" -import { ArrowRight, XCircle } from "lucide-react" -import Image from "next/image" -import { useRouter, useSearchParams } from "next/navigation" -import { Suspense, useEffect, useMemo, useState } from "react" - -import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" - -const API_URL = - process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" - -function isValidLocalhostCallback(callback: string): boolean { - try { - const url = new URL(callback) - const isLocalhost = - url.hostname === "localhost" || url.hostname === "127.0.0.1" - const isHttp = url.protocol === "http:" - const isCallbackPath = url.pathname === "/callback" - return isLocalhost && isHttp && isCallbackPath - } catch { - return false - } -} - -interface PluginInfo { - name: string - description: string - features: string[] - icon: string -} - -const PLUGIN_INFO: Record = { - claude_code: { - name: "Claude Code", - description: - "Persistent memory for Claude Code. Remembers your coding context, patterns, and decisions across sessions.", - features: [ - "Auto-recalls relevant context at session start", - "Captures important observations from tool usage", - "Builds persistent user profile from interactions", - ], - icon: "/images/plugins/claude-code.svg", - }, - opencode: { - name: "OpenCode", - description: - "Memory layer for OpenCode. Enhances your coding assistant with long-term memory capabilities.", - features: [ - "Semantic search across previous sessions", - "Auto-capture of coding decisions", - "Context injection before each prompt", - ], - icon: "/images/plugins/opencode.svg", - }, - openclaw: { - name: "OpenClaw", - description: - "Multi-platform memory for OpenClaw. Works across Telegram, WhatsApp, Discord, Slack and more.", - features: [ - "Cross-channel memory persistence", - "Automatic conversation capture", - "User profile building across platforms", - ], - icon: "/images/plugins/openclaw.svg", - }, - hermes: { - name: "Hermes", - description: "Memory layer for Hermes agent", - features: [ - "Semantic search across previous sessions", - "Auto-capture of conversation context", - "Builds persistent user profile from interactions", - ], - icon: "/images/plugins/hermes.svg", - }, - cursor: { - name: "Cursor", - description: - "Memory layer for Cursor. Enhances your AI coding assistant with persistent context.", - features: [ - "Remembers coding patterns across sessions", - "Auto-capture of project decisions", - "Context-aware suggestions", - ], - icon: "/images/plugins/cursor.png", - }, - codex: { - name: "OpenAI Codex", - description: - "Persistent memory for OpenAI Codex CLI. Remembers your coding context, patterns, and decisions across sessions.", - features: [ - "Auto-recalls relevant context before each prompt", - "Captures coding decisions and patterns automatically", - "Builds persistent user profile across projects", - ], - icon: "/images/plugins/codex.png", - }, -} - -const MULTI_PLUGIN_FEATURES = [ - "Share one persistent memory layer across selected coding agents.", - "Recall project context, coding decisions, and prior sessions.", - "Connect every selected plugin with one approval.", -] - -function isKnownPlugin(value: string): boolean { - return Object.hasOwn(PLUGIN_INFO, value) -} - -function getPluginName(client: string): string { - return PLUGIN_INFO[client]?.name ?? "External Tool" -} - -function formatPluginNames(clients: string[]): string { - const names = clients.map((id) => getPluginName(id)) - if (names.length === 0) return "External Tool" - if (names.length === 1) return names[0] ?? "External Tool" - if (names.length === 2) { - return `${names[0] ?? "External Tool"} and ${names[1] ?? "External Tool"}` - } - - return `${names.slice(0, -1).join(", ")}, and ${names.at(-1) ?? "External Tool"}` -} - -function encodeBase64UrlJson(value: Record): string { - return btoa(JSON.stringify(value)) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, "") -} - -function PluginLogoStack({ clients }: { clients: string[] }) { - if (clients.length === 0) { - return ( -
- -
- ) - } - - return ( -
- {clients.map((id, index) => { - const plugin = PLUGIN_INFO[id] - return ( -
- {plugin ? ( - {plugin.name} - ) : ( - - )} -
- ) - })} -
- ) -} - -type Status = "loading" | "creating" | "success" | "error" - -const pageWrapperClass = - "flex items-center justify-center min-h-screen bg-background p-4" -const cardClass = cn( - "bg-[#14161A] rounded-[14px] p-6 w-full max-w-[400px]", - "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", -) - -function AuthConnectContent() { - const params = useSearchParams() - const router = useRouter() - const { data: session, isPending } = useSession() - const { org, organizations, isRestoring } = useAuth() - const [status, setStatus] = useState("loading") - const [error, setError] = useState(null) - - const callback = params.get("callback") - const client = params.get("client") - const clientsParam = params.get("clients") - const hasClientList = params.has("clients") - const rawRequestedClients = useMemo( - () => - (clientsParam !== null ? clientsParam.split(",") : client ? [client] : []) - .map((value) => value.trim()) - .filter(Boolean), - [client, clientsParam], - ) - const requestedClients = useMemo( - () => Array.from(new Set(rawRequestedClients.filter(isKnownPlugin))), - [rawRequestedClients], - ) - const invalidClients = useMemo( - () => rawRequestedClients.filter((value) => !isKnownPlugin(value)), - [rawRequestedClients], - ) - const validClient = requestedClients[0] ?? null - const displayName = formatPluginNames(requestedClients) - const pluginInfo = - requestedClients.length === 1 && validClient - ? PLUGIN_INFO[validClient] - : null - - // Redirect new users (logged in but no organization) to onboarding. - // Store the current connect URL so onboarding can redirect back here. - const shouldRedirectToOnboarding = - !isPending && - !isRestoring && - !!session && - Array.isArray(organizations) && - organizations.length === 0 - - useEffect(() => { - if (isPending || isRestoring) return - if (!session) return - if (organizations === null) return // orgs query still pending - if (organizations.length > 0) return // has orgs, nothing to do - - try { - sessionStorage.setItem(PENDING_CONNECT_URL_KEY, window.location.href) - } catch (e) { - console.warn("Failed to access sessionStorage for pending connect URL", e) - } - router.replace("/onboarding") - }, [isPending, isRestoring, session, organizations, router]) - - async function handleConnect() { - if (!callback) { - setStatus("error") - setError("Missing callback parameter.") - return - } - if (!isValidLocalhostCallback(callback)) { - setStatus("error") - setError("Invalid callback URL.") - return - } - if (invalidClients.length > 0) { - setStatus("error") - setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`) - return - } - if (requestedClients.length === 0) { - setStatus("error") - setError("Invalid or missing client.") - return - } - if (!session || !org) { - setStatus("error") - setError( - "Your account is not fully set up yet. Please complete onboarding first.", - ) - return - } - - try { - setStatus("creating") - const fetchParams = new URLSearchParams({ callback }) - fetchParams.set("client", requestedClients[0] ?? "") - - const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { - credentials: "include", - }) - - if (!res.ok) { - const errorData = (await res.json().catch(() => ({}))) as { - message?: string - } - throw new Error(errorData.message || "Failed to get API key") - } - - const data = (await res.json()) as { key: string } - setStatus("success") - - const redirectUrl = new URL(callback) - if (hasClientList) { - redirectUrl.searchParams.set( - "keys", - encodeBase64UrlJson( - Object.fromEntries( - requestedClients.map((requestedClient) => [ - requestedClient, - data.key, - ]), - ), - ), - ) - } else { - redirectUrl.searchParams.set("apikey", data.key) - } - redirectUrl.searchParams.set("api_url", API_URL) - window.location.href = redirectUrl.toString() - } catch (err) { - console.error("Failed to get API key:", err) - setStatus("error") - setError(err instanceof Error ? err.message : "Failed to get API key") - } - } - - // Show a spinner while session/org data is loading or while we're about - // to redirect to onboarding (prevents a brief flash of the connect card). - const isAuthLoading = isPending || isRestoring || organizations === null - - useEffect(() => { - if (status !== "loading") return - if (rawRequestedClients.length === 0) { - setStatus("error") - setError("Invalid or missing client.") - return - } - if (invalidClients.length > 0) { - setStatus("error") - setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`) - } - }, [invalidClients, rawRequestedClients.length, status]) - - if (isAuthLoading || shouldRedirectToOnboarding) { - return ( -
-
-
- ) - } - - if (status === "loading") { - return ( -
-
-
- -
-

- Connect {displayName} -

-

- {pluginInfo?.description ?? - (requestedClients.length > 1 - ? "Use one Supermemory account across these plugins." - : `Use your Supermemory account with ${displayName}.`)} -

-
- -
    - {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map( - (feature) => ( -
  • - - - {feature} - -
  • - ), - )} -
- - -
-
-
- ) - } - if (status === "error") { - return ( -
-
-
- -
-

- Connection failed -

-

- {error} -

-
- -
- - - Go to app - -
-
-
-
- ) - } - - return ( -
-
-
-

- {status === "creating" && `Connecting ${displayName}…`} - {status === "success" && - `Success! Redirecting back to ${displayName}…`} -

-
-
- ) -} - -export default function AuthConnectPage() { - return ( - -
-
- } - > - -
- ) -} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 89587227..1a36fca2 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -31,6 +31,23 @@ export default async function proxy(request: Request) { return NextResponse.next() } + if ( + url.pathname === "/auth/connect" || + url.pathname === "/auth/agent-connect" + ) { + const target = new URL(url.toString()) + const labels = url.hostname.split(".") + const appLabel = labels.indexOf("app") + if (appLabel !== -1) { + labels[appLabel] = "console" + target.hostname = labels.join(".") + } else { + target.hostname = "console.supermemory.ai" + } + target.pathname = "/auth/connect" + return NextResponse.redirect(target, 308) + } + const sessionCookie = getAuthSessionCookie(request) console.debug("[PROXY] Session cookie exists:", !!sessionCookie)