From d778021da3de2fce6488dae8b589781adfcb2422 Mon Sep 17 00:00:00 2001 From: Dylan Boudro <121908331+starmorph@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:18:28 -0700 Subject: [PATCH] feat: Auth from Stream --> Middleware (#280) * init auth stream --> middleware * minor cleanup * fix build * fix github token * minor format * handle access token + GH install key * add prompt to install GH app * fix build * fix gh install token passing * remove console logs * CR * CR * move dep, rm private key string replace * CR minor fixes * Update apps/web/src/providers/client.ts --------- Co-authored-by: Brace Sproul --- apps/open-swe/src/security/auth.ts | 5 +- apps/open-swe/src/utils/github/types.ts | 8 +- apps/web/src/app/api/[..._path]/route.ts | 12 +- .../src/app/api/auth/github/callback/route.ts | 8 +- .../src/app/api/auth/github/login/route.ts | 2 +- apps/web/src/app/api/auth/user/route.ts | 34 ++ .../api/github/installation-callback/route.ts | 2 +- .../api/github/installation/callback/route.ts | 2 +- .../app/api/github/proxy/[..._path]/route.ts | 2 +- .../src/app/api/github/repositories/route.ts | 2 +- apps/web/src/app/api/github/token/route.ts | 2 +- apps/web/src/app/github/page.tsx | 4 +- apps/web/src/app/page.tsx | 2 +- .../web/src/components/github/auth-status.tsx | 262 ++++++++++++++ .../components/github/installation-banner.tsx | 56 +++ .../components/github/installation-prompt.tsx | 76 ++++ .../repository-selector.tsx | 4 +- .../src/components/github/repository-list.tsx | 12 +- .../src/components/sidebar-buttons/index.tsx | 3 + apps/web/src/components/thread/index.tsx | 7 +- apps/web/src/components/v2/default-view.tsx | 15 +- apps/web/src/lib/auth.ts | 9 +- apps/web/src/middleware.ts | 18 + apps/web/src/providers/Stream.tsx | 324 +----------------- packages/shared/package.json | 2 + packages/shared/src/constants.ts | 3 + .../shared/src/github/verify-user.ts | 12 +- yarn.lock | 18 + 28 files changed, 534 insertions(+), 372 deletions(-) create mode 100644 apps/web/src/app/api/auth/user/route.ts create mode 100644 apps/web/src/components/github/auth-status.tsx create mode 100644 apps/web/src/components/github/installation-banner.tsx create mode 100644 apps/web/src/components/github/installation-prompt.tsx create mode 100644 apps/web/src/middleware.ts rename apps/open-swe/src/security/github-auth.ts => packages/shared/src/github/verify-user.ts (68%) diff --git a/apps/open-swe/src/security/auth.ts b/apps/open-swe/src/security/auth.ts index ad37085d..0f902a7a 100644 --- a/apps/open-swe/src/security/auth.ts +++ b/apps/open-swe/src/security/auth.ts @@ -1,5 +1,8 @@ import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; -import { verifyGithubUser, GithubUser } from "./github-auth.js"; +import { + verifyGithubUser, + GithubUser, +} from "@open-swe/shared/github/verify-user"; import { GITHUB_INSTALLATION_TOKEN_COOKIE, GITHUB_TOKEN_COOKIE, diff --git a/apps/open-swe/src/utils/github/types.ts b/apps/open-swe/src/utils/github/types.ts index b9249264..f159de3a 100644 --- a/apps/open-swe/src/utils/github/types.ts +++ b/apps/open-swe/src/utils/github/types.ts @@ -1,10 +1,10 @@ -import type { Endpoints } from "@octokit/types"; +import type { RestEndpointMethodTypes } from "@octokit/rest"; export type GitHubIssue = - Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}"]["response"]["data"]; + RestEndpointMethodTypes["issues"]["get"]["response"]["data"]; export type GitHubIssueComment = - Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}/comments"]["response"]["data"][number]; + RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][number]; export type GitHubPullRequest = - Endpoints["POST /repos/{owner}/{repo}/pulls"]["response"]["data"]; + RestEndpointMethodTypes["pulls"]["create"]["response"]["data"]; diff --git a/apps/web/src/app/api/[..._path]/route.ts b/apps/web/src/app/api/[..._path]/route.ts index a50a7fb9..e01abceb 100644 --- a/apps/web/src/app/api/[..._path]/route.ts +++ b/apps/web/src/app/api/[..._path]/route.ts @@ -1,12 +1,12 @@ import { initApiPassthrough } from "langgraph-nextjs-api-passthrough"; import { GITHUB_TOKEN_COOKIE, + GITHUB_INSTALLATION_ID_COOKIE, GITHUB_INSTALLATION_TOKEN_COOKIE, } from "@open-swe/shared/constants"; import { encryptGitHubToken } from "@open-swe/shared/crypto"; import { NextRequest } from "next/server"; import { getInstallationToken } from "@/utils/github"; -import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth"; function getGitHubAccessTokenOrThrow( req: NextRequest, @@ -15,7 +15,9 @@ function getGitHubAccessTokenOrThrow( const token = req.cookies.get(GITHUB_TOKEN_COOKIE)?.value ?? ""; if (!token) { - throw new Error("No GitHub access token cookie found."); + throw new Error( + "No GitHub access token found. User must authenticate first.", + ); } return encryptGitHubToken(token, encryptionKey); @@ -28,12 +30,16 @@ async function getGitHubInstallationTokenOrThrow( const installationIdCookie = req.cookies.get( GITHUB_INSTALLATION_ID_COOKIE, )?.value; + if (!installationIdCookie) { - throw new Error("No GitHub installation ID cookie found."); + throw new Error( + "No GitHub installation ID found. GitHub App must be installed first.", + ); } const appId = process.env.GITHUB_APP_ID; const privateAppKey = process.env.GITHUB_APP_PRIVATE_KEY; + if (!appId || !privateAppKey) { throw new Error("GitHub App ID or Private App Key is not configured."); } diff --git a/apps/web/src/app/api/auth/github/callback/route.ts b/apps/web/src/app/api/auth/github/callback/route.ts index fa064790..b4d35185 100644 --- a/apps/web/src/app/api/auth/github/callback/route.ts +++ b/apps/web/src/app/api/auth/github/callback/route.ts @@ -2,9 +2,9 @@ import { GITHUB_AUTH_STATE_COOKIE, GITHUB_INSTALLATION_ID_COOKIE, GITHUB_TOKEN_TYPE_COOKIE, -} from "@/lib/auth"; + GITHUB_TOKEN_COOKIE, +} from "@open-swe/shared/constants"; import { NextRequest, NextResponse } from "next/server"; -import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants"; export async function GET(request: NextRequest) { try { @@ -81,9 +81,7 @@ export async function GET(request: NextRequest) { } // Create the success response - const response = NextResponse.redirect( - new URL("/?auth=success", request.url), - ); + const response = NextResponse.redirect(new URL("/chat", request.url)); // Clear the state cookie as it's no longer needed response.cookies.set(GITHUB_AUTH_STATE_COOKIE, "", { diff --git a/apps/web/src/app/api/auth/github/login/route.ts b/apps/web/src/app/api/auth/github/login/route.ts index 0fb0e35a..ac4e3670 100644 --- a/apps/web/src/app/api/auth/github/login/route.ts +++ b/apps/web/src/app/api/auth/github/login/route.ts @@ -1,4 +1,4 @@ -import { GITHUB_AUTH_STATE_COOKIE } from "@/lib/auth"; +import { GITHUB_AUTH_STATE_COOKIE } from "@open-swe/shared/constants"; import { NextRequest, NextResponse } from "next/server"; export async function GET(_request: NextRequest) { diff --git a/apps/web/src/app/api/auth/user/route.ts b/apps/web/src/app/api/auth/user/route.ts new file mode 100644 index 00000000..41399e86 --- /dev/null +++ b/apps/web/src/app/api/auth/user/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getGitHubToken } from "@/lib/auth"; +import { verifyGithubUser } from "@open-swe/shared/github/verify-user"; + +export async function GET(request: NextRequest) { + try { + const token = getGitHubToken(request); + if (!token || !token.access_token) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + const user = await verifyGithubUser(token.access_token); + if (!user) { + return NextResponse.json( + { error: "Invalid GitHub token" }, + { status: 401 }, + ); + } + // Only return safe fields + return NextResponse.json({ + user: { + login: user.login, + avatar_url: user.avatar_url, + html_url: user.html_url, + name: user.name, + email: user.email, + }, + }); + } catch (error) { + return NextResponse.json( + { error: "Failed to fetch user info" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/api/github/installation-callback/route.ts b/apps/web/src/app/api/github/installation-callback/route.ts index 234a48cf..6be0c98d 100644 --- a/apps/web/src/app/api/github/installation-callback/route.ts +++ b/apps/web/src/app/api/github/installation-callback/route.ts @@ -1,5 +1,5 @@ +import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; import { - GITHUB_INSTALLATION_ID_COOKIE, GITHUB_INSTALLATION_RETURN_TO_COOKIE, GITHUB_INSTALLATION_STATE_COOKIE, } from "@/lib/auth"; diff --git a/apps/web/src/app/api/github/installation/callback/route.ts b/apps/web/src/app/api/github/installation/callback/route.ts index 30045f65..3134a8e5 100644 --- a/apps/web/src/app/api/github/installation/callback/route.ts +++ b/apps/web/src/app/api/github/installation/callback/route.ts @@ -1,4 +1,4 @@ -import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth"; +import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; import { NextRequest, NextResponse } from "next/server"; /** diff --git a/apps/web/src/app/api/github/proxy/[..._path]/route.ts b/apps/web/src/app/api/github/proxy/[..._path]/route.ts index 077901f9..a05dbb4e 100644 --- a/apps/web/src/app/api/github/proxy/[..._path]/route.ts +++ b/apps/web/src/app/api/github/proxy/[..._path]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getInstallationToken } from "../../../../../utils/github"; // Adjusted path -import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth"; +import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; const GITHUB_API_URL = "https://api.github.com"; diff --git a/apps/web/src/app/api/github/repositories/route.ts b/apps/web/src/app/api/github/repositories/route.ts index 54c0816a..99d7bf52 100644 --- a/apps/web/src/app/api/github/repositories/route.ts +++ b/apps/web/src/app/api/github/repositories/route.ts @@ -4,7 +4,7 @@ import { getInstallationRepositories, Repository, } from "@/utils/github"; -import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth"; +import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; /** * Fetches repositories accessible to the GitHub App installation diff --git a/apps/web/src/app/api/github/token/route.ts b/apps/web/src/app/api/github/token/route.ts index 64307ff7..1524cd81 100644 --- a/apps/web/src/app/api/github/token/route.ts +++ b/apps/web/src/app/api/github/token/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getInstallationToken } from "@/utils/github"; -import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth"; +import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants"; /** * Returns a GitHub installation token that can be used for Git operations diff --git a/apps/web/src/app/github/page.tsx b/apps/web/src/app/github/page.tsx index c033a186..1742593b 100644 --- a/apps/web/src/app/github/page.tsx +++ b/apps/web/src/app/github/page.tsx @@ -68,7 +68,7 @@ export default function GitHubPage() { )} {!isInstalled ? ( -
+

Install GitHub App

To access your GitHub repositories, you need to install our GitHub @@ -76,7 +76,7 @@ export default function GitHubPage() {

diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index ce132525..860ac08e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,8 +1,8 @@ "use client"; import { Thread } from "@/components/thread"; -import { StreamProvider } from "@/providers/Stream"; import { ThreadProvider } from "@/providers/Thread"; +import { StreamProvider } from "@/providers/Stream"; import { Toaster } from "@/components/ui/sonner"; import React from "react"; import { GitHubAppProvider } from "@/providers/GitHubApp"; diff --git a/apps/web/src/components/github/auth-status.tsx b/apps/web/src/components/github/auth-status.tsx new file mode 100644 index 00000000..bb0e263c --- /dev/null +++ b/apps/web/src/components/github/auth-status.tsx @@ -0,0 +1,262 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { GitHubSVG } from "@/components/icons/github"; +import { LogOut } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +const GITHUB_APP_INSTALL_URL = "/api/github/installation"; +const GITHUB_LOGIN_URL = "/api/auth/github/login"; +const GITHUB_LOGOUT_URL = "/api/auth/logout"; + +interface GitHubUser { + login: string; + avatar_url: string; + html_url: string; + name?: string; + email?: string; +} + +export default function AuthStatus() { + const [isAuth, setIsAuth] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isInstalled, setIsInstalled] = useState(null); + const [isInstallLoading, setIsInstallLoading] = useState(false); + const [error, setError] = useState(null); + const [user, setUser] = useState(null); + const [userLoading, setUserLoading] = useState(false); + const [popoverOpen, setPopoverOpen] = useState(false); + + useEffect(() => { + checkAuthStatus(); + }, []); + + useEffect(() => { + if (isAuth) { + checkAppInstallation(); + fetchGitHubUser(); + } else { + setIsInstalled(null); + setUser(null); + } + }, [isAuth]); + + const checkAuthStatus = async () => { + setIsLoading(true); + try { + const response = await fetch("/api/auth/status"); + const data = await response.json(); + setIsAuth(data.authenticated); + } catch (err) { + setIsAuth(false); + } finally { + setIsLoading(false); + } + }; + + const checkAppInstallation = async () => { + setIsInstallLoading(true); + try { + const response = await fetch("/api/github/repositories"); + if (response.ok) { + setIsInstalled(true); + } else { + const data = await response.json(); + if (data.error && data.error.includes("installation")) { + setIsInstalled(false); + } else { + setError(data.error || "Unknown error"); + } + } + } catch (err) { + setError("Failed to check GitHub App installation status"); + } finally { + setIsInstallLoading(false); + } + }; + + const fetchGitHubUser = async () => { + setUserLoading(true); + try { + const response = await fetch("/api/auth/user"); + if (response.ok) { + const data = await response.json(); + setUser(data.user); + } else { + setUser(null); + } + } catch { + setUser(null); + } finally { + setUserLoading(false); + } + }; + + const handleLogin = () => { + window.location.href = GITHUB_LOGIN_URL; + }; + + const handleLogout = async () => { + setIsLoading(true); + try { + await fetch(GITHUB_LOGOUT_URL, { method: "POST" }); + window.location.href = "/"; + } catch { + setIsLoading(false); + } + }; + + const handleInstall = () => { + window.location.href = GITHUB_APP_INSTALL_URL; + }; + + return ( +
+ {isLoading ? ( + + ) : isAuth ? ( + + + + + +
+ {userLoading ? ( +
+ +
+
+
+
+
+ ) : user ? ( +
+ {user.login} +
+
+ {user.name || user.login} +
+
+ {user.email || user.login} +
+
+
+ ) : null} + {isInstallLoading ? ( + + ) : isInstalled === false ? ( + + ) : isInstalled === true ? ( + + {" "} + App Installed + + ) : null} + +
+ + + ) : ( + + )} + {error && {error}} +
+ ); +} diff --git a/apps/web/src/components/github/installation-banner.tsx b/apps/web/src/components/github/installation-banner.tsx new file mode 100644 index 00000000..2d117c0d --- /dev/null +++ b/apps/web/src/components/github/installation-banner.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useGitHubAppProvider } from "@/providers/GitHubApp"; +import { InstallationPrompt } from "./installation-prompt"; +import { useState, useEffect } from "react"; +import { cn } from "@/lib/utils"; + +export function GitHubInstallationBanner() { + const { isInstalled, isLoading } = useGitHubAppProvider(); + const [dismissed, setDismissed] = useState(false); + const [isNewUser, setIsNewUser] = useState(false); + + useEffect(() => { + // Check if this might be a new user (no installation history in localStorage) + const hasSeenInstallation = localStorage.getItem( + "github_installation_seen", + ); + if (!hasSeenInstallation && !isInstalled && !isLoading) { + setIsNewUser(true); + localStorage.setItem("github_installation_seen", "true"); + } + }, [isInstalled, isLoading]); + + // Don't show banner if: + // - Still loading installation status + // - App is already installed + // - User has dismissed the banner + if (isLoading || isInstalled || dismissed) { + return null; + } + + const handleDismiss = () => { + setDismissed(true); + setIsNewUser(false); + }; + + // Enhanced messaging for new users + const title = isNewUser + ? "🎉 Welcome to Open SWE! Complete your setup" + : "Complete your setup to start coding"; + + const description = isNewUser + ? "You're just one step away from AI-powered development! Install our GitHub App to connect your repositories and start coding with AI assistance." + : "Install our GitHub App to grant access to your repositories and enable AI-powered development."; + + return ( + + ); +} diff --git a/apps/web/src/components/github/installation-prompt.tsx b/apps/web/src/components/github/installation-prompt.tsx new file mode 100644 index 00000000..cb7dc655 --- /dev/null +++ b/apps/web/src/components/github/installation-prompt.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { InstallAppButton } from "./install-app-button"; +import { X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface InstallationPromptProps { + title?: string; + description?: string; + showDismiss?: boolean; + onDismiss?: () => void; + className?: string; + variant?: "default" | "banner"; +} + +export function InstallationPrompt({ + title = "GitHub App Not Installed", + description = "You need to install our GitHub App to grant access to your repositories.", + showDismiss = false, + onDismiss, + className = "", + variant = "default", +}: InstallationPromptProps) { + return ( +
+ {variant === "banner" ? ( + <> +
+

+ {title} +

+

+ {description} +

+
+
+ + Install GitHub App + + {showDismiss && onDismiss && ( + + )} +
+ + ) : ( + <> +

+ {title} +

+

+ {description} +

+ Install GitHub App + + )} +
+ ); +} diff --git a/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx b/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx index 4925698f..9fd349a5 100644 --- a/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx +++ b/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx @@ -16,9 +16,9 @@ import { Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { useState } from "react"; import type { TargetRepository } from "@open-swe/shared/open-swe/types"; -import { useGitHubAppProvider } from "@/providers/GitHubApp"; -import type { Repository } from "@/utils/github"; import { GitHubSVG } from "@/components/icons/github"; +import { Repository } from "@/utils/github"; +import { useGitHubAppProvider } from "@/providers/GitHubApp"; interface RepositorySelectorProps { disabled?: boolean; diff --git a/apps/web/src/components/github/repository-list.tsx b/apps/web/src/components/github/repository-list.tsx index 9bc0110f..7d7238f4 100644 --- a/apps/web/src/components/github/repository-list.tsx +++ b/apps/web/src/components/github/repository-list.tsx @@ -1,7 +1,6 @@ "use client"; - import { useGitHubAppProvider } from "@/providers/GitHubApp"; -import { InstallAppButton } from "./install-app-button"; +import { InstallationPrompt } from "./installation-prompt"; import { Button } from "@/components/ui/button"; import { RefreshCw } from "lucide-react"; @@ -49,14 +48,7 @@ export function RepositoryList({ className = "" }: RepositoryListProps) { if (!isInstalled) { return (
-
-

GitHub App Not Installed

-

- You need to install our GitHub App to grant access to your - repositories. -

- Install GitHub App -
+
); } diff --git a/apps/web/src/components/sidebar-buttons/index.tsx b/apps/web/src/components/sidebar-buttons/index.tsx index e17ed158..4cd2125a 100644 --- a/apps/web/src/components/sidebar-buttons/index.tsx +++ b/apps/web/src/components/sidebar-buttons/index.tsx @@ -9,6 +9,7 @@ import { import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { TooltipIconButton } from "@/components/ui/tooltip-icon-button"; +import AuthStatus from "@/components/github/auth-status"; interface SidebarButtonsProps { historyOpen: boolean; @@ -17,6 +18,7 @@ interface SidebarButtonsProps { setConfigOpen: (open: boolean) => void; className?: string; } + // TODO: Implement Using this component in the thread component instead of current implementation export const SidebarButtons = forwardRef( ( @@ -104,6 +106,7 @@ export const SidebarButtons = forwardRef( > +
diff --git a/apps/web/src/components/thread/index.tsx b/apps/web/src/components/thread/index.tsx index 2c730f47..3b02a2f0 100644 --- a/apps/web/src/components/thread/index.tsx +++ b/apps/web/src/components/thread/index.tsx @@ -17,7 +17,6 @@ import { PanelRightOpen, PanelRightClose, SquarePen, - XIcon, Settings, FilePlus2, } from "lucide-react"; @@ -30,7 +29,6 @@ import { useMediaQuery } from "@/hooks/useMediaQuery"; import { Label } from "../ui/label"; import { useFileUpload } from "@/hooks/useFileUpload"; import { ContentBlocksPreview } from "./ContentBlocksPreview"; -import { GitHubOAuthButton } from "../github/github-oauth-button"; import { useGitHubAppProvider } from "@/providers/GitHubApp"; import TaskList from "../task-list"; import { ConfigurationSidebar } from "../configuration-sidebar"; @@ -54,6 +52,7 @@ import { mapCustomEventsToSteps, } from "@open-swe/shared/open-swe/custom-node-events"; import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants"; +import AuthStatus from "../github/auth-status"; function StickyToBottomContent(props: { content: ReactNode; @@ -403,6 +402,7 @@ export function Thread() { )}
+ -
)} @@ -467,7 +466,7 @@ export function Thread() {
- +
+ {/* Terminal Chat Input */} 0) { + const url = request.nextUrl.clone(); + url.pathname = "/chat"; + return NextResponse.redirect(url); + } + } + return NextResponse.next(); +} + +export const config = { + matcher: ["/"], +}; diff --git a/apps/web/src/providers/Stream.tsx b/apps/web/src/providers/Stream.tsx index af080c3e..e87f6dba 100644 --- a/apps/web/src/providers/Stream.tsx +++ b/apps/web/src/providers/Stream.tsx @@ -1,10 +1,4 @@ -import React, { - createContext, - useContext, - ReactNode, - useState, - useEffect, -} from "react"; +import React, { createContext, useContext, ReactNode, useState } from "react"; import { useStream } from "@langchain/langgraph-sdk/react"; import { uiMessageReducer, @@ -14,14 +8,7 @@ import { type RemoveUIMessage, } from "@langchain/langgraph-sdk/react-ui"; import { useQueryState } from "nuqs"; -import { LangGraphLogoSVG } from "@/components/icons/langgraph"; import { useThreadsContext } from "./Thread"; -import { TooltipIconButton } from "@/components/ui/tooltip-icon-button"; -import { Copy, CopyCheck, ArrowRight } from "lucide-react"; -import { motion } from "framer-motion"; -import { Button } from "@/components/ui/button"; -import { GitHubSVG } from "@/components/icons/github"; -import { useGitHubToken } from "@/hooks/useGitHubToken"; import { GraphState, GraphUpdate } from "@open-swe/shared/open-swe/types"; import { CustomNodeEvent, @@ -45,23 +32,22 @@ async function sleep(ms = 4000) { return new Promise((resolve) => setTimeout(resolve, ms)); } -const StreamSession = ({ - children, - apiUrl, - assistantId, - githubToken, -}: { - children: ReactNode; - apiUrl: string; - assistantId: string; - githubToken: string; -}) => { +const StreamSession = ({ children }: { children: ReactNode }) => { const [threadId, setThreadId] = useQueryState("threadId"); const [customEvents, setCustomEvents] = useState([]); const { refreshThreads } = useThreadsContext(); + const streamValue = useTypedStream({ - apiUrl, - assistantId, + apiUrl: + process.env.NEXT_PUBLIC_API_URL ?? + (() => { + throw new Error("NEXT_PUBLIC_API_URL is required"); + })(), + assistantId: + process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID ?? + (() => { + throw new Error("NEXT_PUBLIC_MANAGER_ASSISTANT_ID is required"); + })(), reconnectOnMount: true, threadId: threadId ?? null, onCustomEvent: (event, options) => { @@ -77,7 +63,6 @@ const StreamSession = ({ }, onThreadId: (id) => { setThreadId(id); - sleep().then(() => { refreshThreads().catch(console.error); }); @@ -96,285 +81,10 @@ const StreamSession = ({ ); }; -export const StreamProvider: React.FC<{ children: ReactNode }> = ({ - children, -}) => { - const baseCopyTooltipText = "Copy environment variables"; - const [copyTooltipText, setCopyTooltipText] = useState(baseCopyTooltipText); - - const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? ""; - const assistantId: string | undefined = - process.env.NEXT_PUBLIC_ASSISTANT_ID ?? ""; - - const [isAuth, setIsAuth] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const [hasGitHubAppInstalled, setHasGitHubAppInstalled] = useState< - boolean | null - >(() => { - if (typeof window === "undefined") return null; - const cached = localStorage.getItem("github_app_installed"); - return cached === "true" ? true : cached === "false" ? false : null; - }); - - const [isCheckingAppInstallation, setIsCheckingAppInstallation] = - useState(false); - const { - token: githubToken, - fetchToken: fetchGitHubToken, - isLoading: isTokenLoading, - } = useGitHubToken(); - - useEffect(() => { - checkAuthStatus(); - }, []); - - useEffect(() => { - if (isAuth) { - const cachedInstallationStatus = localStorage.getItem( - "github_app_installed", - ); - if (cachedInstallationStatus === "true") { - setHasGitHubAppInstalled(true); - // Fetch token if we don't have one yet - if (!githubToken && !isTokenLoading) { - fetchGitHubToken(); - } - } else if (cachedInstallationStatus === "false") { - setHasGitHubAppInstalled(false); - } else { - checkGitHubAppInstallation(); - } - } - }, [isAuth, githubToken]); - - const checkAuthStatus = async () => { - try { - const response = await fetch("/api/auth/status"); - const data = await response.json(); - setIsAuth(data.authenticated); - } catch (error) { - console.error("Error checking auth status:", error); - setIsAuth(false); - } - }; - - const checkGitHubAppInstallation = async () => { - setIsCheckingAppInstallation(true); - try { - const response = await fetch("/api/github/repositories"); - if (response.ok) { - setHasGitHubAppInstalled(true); - localStorage.setItem("github_app_installed", "true"); - - await fetchGitHubToken(); - } else { - const errorData = await response.json(); - if (errorData.error.includes("installation")) { - setHasGitHubAppInstalled(false); - localStorage.setItem("github_app_installed", "false"); - } else { - setHasGitHubAppInstalled(false); - localStorage.setItem("github_app_installed", "false"); - } - } - } catch (error) { - console.error("Error checking GitHub App installation:", error); - setHasGitHubAppInstalled(false); - localStorage.setItem("github_app_installed", "false"); - } finally { - setIsCheckingAppInstallation(false); - } - }; - - const handleLogin = () => { - setIsLoading(true); - window.location.href = "/api/auth/github/login"; - }; - - const handleInstallGitHubApp = () => { - setIsLoading(true); - - localStorage.removeItem("github_app_installed"); - window.location.href = "/api/github/installation"; - }; - - if (!apiUrl || !assistantId) { - return ( -
-
-
-
- -

- Environment Variables Missing -

-
-

- Whoops, looks like you don't have an API URL or assistant ID - set in your environment variables. Please make sure you have both - of these set before continuing. -

-
- { - const textToCopy = `NEXT_PUBLIC_API_URL=${apiUrl}\nNEXT_PUBLIC_ASSISTANT_ID=${assistantId}`; - navigator.clipboard.writeText(textToCopy).then(() => { - setCopyTooltipText("Copied!"); - setTimeout( - () => setCopyTooltipText(baseCopyTooltipText), - 2000, - ); - }); - }} - className="absolute top-2 right-2 cursor-pointer" - tooltip={copyTooltipText} - > - {copyTooltipText === baseCopyTooltipText ? ( - - - - ) : ( - - - - )} - - - NEXT_PUBLIC_API_URL={apiUrl} - NEXT_PUBLIC_ASSISTANT_ID={assistantId} - -
-
-
-
- ); - } - - if (!isAuth) { - return ( -
-
-
-
- -

- Get started -

-
-

- Connect your GitHub account to get started with Open SWE. -

- -
-
-
- ); - } - - // Step 2: GitHub App Installation (only show if authenticated but app not installed) - // Only show modal if we've definitively determined the app is not installed - // Don't show while we're still loading or have cached positive status - if (isAuth && hasGitHubAppInstalled === false && !isTokenLoading) { - return ( -
-
-
-
- -

- One more step -

-
-
- - 1. GitHub Login ✓ - - - - 2. Repository Access - -
-

- Great! Now we need access to your GitHub repositories. Install our - GitHub App to grant access to specific repositories. -

-
-

- You'll be redirected to GitHub where you can select which - repositories to grant access to. -

-
- -
-
-
- ); - } - - // Only render StreamSession when we have a valid token - if (!githubToken) { - return ( -
-
-
-
- -

- Loading... -

-
-

- Setting up your GitHub integration... -

-
-
-
- ); - } - - return ( - - {children} - - ); +export const StreamProvider: React.FC<{ + children: ReactNode; +}> = ({ children }) => { + return {children}; }; export const useStreamContext = (): StreamContextType => { diff --git a/packages/shared/package.json b/packages/shared/package.json index 726ad561..1f16e400 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -20,11 +20,13 @@ "@langchain/core": "^0.3.56", "@langchain/langgraph": "^0.3.3", "@langchain/langgraph-sdk": "^0.0.83", + "@octokit/rest": "^22.0.0", "zod": "^3.25.32" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.19.0", + "@octokit/types": "^12.0.0", "@tsconfig/recommended": "^1.0.8", "@types/node": "^22.13.5", "dotenv": "^16.4.7", diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 76c027e3..b1cb7043 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -9,3 +9,6 @@ export const GITHUB_TOKEN_COOKIE = "x-github-access-token"; export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token"; export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-"; +export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state"; +export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id"; +export const GITHUB_TOKEN_TYPE_COOKIE = "github_token_type"; diff --git a/apps/open-swe/src/security/github-auth.ts b/packages/shared/src/github/verify-user.ts similarity index 68% rename from apps/open-swe/src/security/github-auth.ts rename to packages/shared/src/github/verify-user.ts index db80f454..2445f951 100644 --- a/apps/open-swe/src/security/github-auth.ts +++ b/packages/shared/src/github/verify-user.ts @@ -1,8 +1,5 @@ import { Octokit } from "@octokit/rest"; import { Endpoints } from "@octokit/types"; -import { createLogger, LogLevel } from "../utils/logger.js"; - -const logger = createLogger(LogLevel.INFO, "GithubAuth"); export type GithubUser = Endpoints["GET /user"]["response"]["data"]; @@ -20,19 +17,12 @@ export async function verifyGithubUser( try { const octokit = new Octokit({ auth: accessToken }); - const { data: user } = await octokit.users.getAuthenticated(); - if (!user || !user.login) { - logger.error( - "GitHub token is invalid or user information could not be retrieved.", - ); return undefined; } - return user; - } catch (error) { - logger.error("An error occurred during GitHub user verification:", error); + } catch { return undefined; } } diff --git a/yarn.lock b/yarn.lock index 690afd61..ab991d5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2130,6 +2130,13 @@ __metadata: languageName: node linkType: hard +"@octokit/openapi-types@npm:^20.0.0": + version: 20.0.0 + resolution: "@octokit/openapi-types@npm:20.0.0" + checksum: 23ff7613750f8b5790a0cbed5a2048728a7909e50d726932831044908357a932c7fc0613fb7b86430a49d31b3d03a180632ea5dd936535bfbc1176391a199e96 + languageName: node + linkType: hard + "@octokit/openapi-types@npm:^25.1.0": version: 25.1.0 resolution: "@octokit/openapi-types@npm:25.1.0" @@ -2202,6 +2209,15 @@ __metadata: languageName: node linkType: hard +"@octokit/types@npm:^12.0.0": + version: 12.6.0 + resolution: "@octokit/types@npm:12.6.0" + dependencies: + "@octokit/openapi-types": ^20.0.0 + checksum: 850235f425584499a2266d5c585c1c2462ae11e25c650567142f3342cb9ce589c8c8fed87705811ca93271fd28c68e1fa77b88b67b97015d7b63d269fa46ed05 + languageName: node + linkType: hard + "@octokit/types@npm:^14.0.0, @octokit/types@npm:^14.1.0": version: 14.1.0 resolution: "@octokit/types@npm:14.1.0" @@ -2284,6 +2300,8 @@ __metadata: "@langchain/core": ^0.3.56 "@langchain/langgraph": ^0.3.3 "@langchain/langgraph-sdk": ^0.0.83 + "@octokit/rest": ^22.0.0 + "@octokit/types": ^12.0.0 "@tsconfig/recommended": ^1.0.8 "@types/node": ^22.13.5 dotenv: ^16.4.7