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() {
Install GitHub App
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 ? (
+
+ {" "}
+ Checking...
+
+ ) : isAuth ? (
+
+
+ setPopoverOpen((open) => !open)}
+ >
+ {userLoading ? (
+
+ ) : user && user.avatar_url ? (
+
+ ) : (
+
+ )}
+ {user && user.login ? (
+ {user.login}
+ ) : (
+ GitHub User
+ )}
+
+
+
+
+ {userLoading ? (
+
+ ) : user ? (
+
+
+
+
+ {user.name || user.login}
+
+
+ {user.email || user.login}
+
+
+
+ ) : null}
+ {isInstallLoading ? (
+
+ {" "}
+ Checking App...
+
+ ) : isInstalled === false ? (
+
+ {" "}
+ Install GitHub App
+
+ ) : isInstalled === true ? (
+
+ {" "}
+ App Installed
+
+ ) : null}
+
+ Logout
+
+
+
+
+ ) : (
+
+ {" "}
+ Connect GitHub
+
+ )}
+ {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.
-
-
-
- {isLoading ? "Connecting..." : "Connect GitHub"}
-
-
-
-
- );
- }
-
- // 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.
-
-
-
-
- {isLoading || isCheckingAppInstallation
- ? "Loading..."
- : "Install GitHub App"}
-
-
-
-
- );
- }
-
- // 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