mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
parent
f6fa1fa4a4
commit
6ac4deacd6
18 changed files with 327 additions and 182 deletions
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from "@open-swe/shared/github/verify-user";
|
||||
import {
|
||||
API_KEY_REQUIRED_MESSAGE,
|
||||
GITHUB_INSTALLATION_ID,
|
||||
GITHUB_INSTALLATION_NAME,
|
||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
|
|
@ -143,6 +144,12 @@ export const auth = new Auth()
|
|||
message: "GitHub installation name header missing",
|
||||
});
|
||||
}
|
||||
const installationIdHeader = request.headers.get(GITHUB_INSTALLATION_ID);
|
||||
if (!installationIdHeader) {
|
||||
throw new HTTPException(401, {
|
||||
message: "GitHub installation ID header missing",
|
||||
});
|
||||
}
|
||||
|
||||
// We don't do anything with this token right now, but still confirm it
|
||||
// exists as it will cause issues later on if it's not present.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
GITHUB_USER_LOGIN_HEADER,
|
||||
GITHUB_INSTALLATION_NAME,
|
||||
GITHUB_PAT,
|
||||
GITHUB_INSTALLATION_ID,
|
||||
} from "@open-swe/shared/constants";
|
||||
|
||||
export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
|
||||
|
|
@ -22,8 +23,13 @@ export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
|
|||
config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE];
|
||||
const githubInstallationName =
|
||||
config.configurable?.[GITHUB_INSTALLATION_NAME];
|
||||
const githubInstallationId = config.configurable?.[GITHUB_INSTALLATION_ID];
|
||||
|
||||
if (!githubInstallationTokenCookie || !githubInstallationName) {
|
||||
if (
|
||||
!githubInstallationTokenCookie ||
|
||||
!githubInstallationName ||
|
||||
!githubInstallationId
|
||||
) {
|
||||
throw new Error("Missing required headers");
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +42,7 @@ export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
|
|||
// Required headers
|
||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: githubInstallationTokenCookie,
|
||||
[GITHUB_INSTALLATION_NAME]: githubInstallationName,
|
||||
[GITHUB_INSTALLATION_ID]: githubInstallationId,
|
||||
|
||||
// Optional headers
|
||||
[GITHUB_TOKEN_COOKIE]: githubTokenCookie,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
GITHUB_INSTALLATION_ID,
|
||||
} from "@open-swe/shared/constants";
|
||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||
|
|
@ -9,6 +10,7 @@ import { getGitHubPatFromConfig } from "./github-pat.js";
|
|||
export function getGitHubTokensFromConfig(config: GraphConfig): {
|
||||
githubAccessToken: string;
|
||||
githubInstallationToken: string;
|
||||
installationId: string;
|
||||
} {
|
||||
if (!config.configurable) {
|
||||
throw new Error("No configurable object found in graph config.");
|
||||
|
|
@ -22,12 +24,20 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
|
|||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
const installationId = config.configurable[GITHUB_INSTALLATION_ID];
|
||||
if (!installationId) {
|
||||
throw new Error(
|
||||
`Missing required ${GITHUB_INSTALLATION_ID} in configuration.`,
|
||||
);
|
||||
}
|
||||
|
||||
const githubPat = getGitHubPatFromConfig(config.configurable, encryptionKey);
|
||||
if (githubPat && !isProd) {
|
||||
// check for PAT-only mode
|
||||
return {
|
||||
githubAccessToken: githubPat,
|
||||
githubInstallationToken: githubPat,
|
||||
installationId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -49,5 +59,5 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
|
|||
encryptionKey,
|
||||
);
|
||||
|
||||
return { githubAccessToken, githubInstallationToken };
|
||||
return { githubAccessToken, githubInstallationToken, installationId };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,43 +2,114 @@ import { Octokit } from "@octokit/rest";
|
|||
import { createLogger, LogLevel } from "../logger.js";
|
||||
import { GitHubIssue, GitHubIssueComment, GitHubPullRequest } from "./types.js";
|
||||
import { getOpenSWELabel } from "./label.js";
|
||||
import { getInstallationToken } from "@open-swe/shared/github/auth";
|
||||
import { getConfig } from "@langchain/langgraph";
|
||||
import { GITHUB_INSTALLATION_ID } from "@open-swe/shared/constants";
|
||||
import { updateConfig } from "../update-config.js";
|
||||
import { encryptSecret } from "@open-swe/shared/crypto";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "GitHub-API");
|
||||
|
||||
async function getInstallationTokenAndUpdateConfig() {
|
||||
try {
|
||||
const config = getConfig();
|
||||
const encryptionSecret = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!encryptionSecret) {
|
||||
throw new Error("Secrets encryption key not found");
|
||||
}
|
||||
|
||||
const installationId = config.configurable?.[GITHUB_INSTALLATION_ID];
|
||||
const appId = process.env.GITHUB_APP_ID;
|
||||
const privateKey = process.env.GITHUB_APP_PRIVATE_KEY;
|
||||
if (!installationId || !appId || !privateKey) {
|
||||
throw new Error(
|
||||
"GitHub installation ID, app ID, or private key not found",
|
||||
);
|
||||
}
|
||||
|
||||
const token = await getInstallationToken(installationId, appId, privateKey);
|
||||
const encryptedToken = encryptSecret(token, encryptionSecret);
|
||||
updateConfig(GITHUB_INSTALLATION_ID, encryptedToken);
|
||||
return token;
|
||||
} catch (e) {
|
||||
logger.error("Failed to get installation token and update config", {
|
||||
error: e,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic utility for handling GitHub API calls with automatic retry on 401 errors
|
||||
*/
|
||||
async function withGitHubRetry<T>(
|
||||
operation: (token: string) => Promise<T>,
|
||||
initialToken: string,
|
||||
errorMessage: string,
|
||||
additionalLogFields?: Record<string, any>,
|
||||
numRetries = 1,
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await operation(initialToken);
|
||||
} catch (error) {
|
||||
const errorFields =
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: {};
|
||||
|
||||
// Retry with a max retries of 2
|
||||
if (errorFields && errorFields.message?.includes("401") && numRetries < 2) {
|
||||
const token = await getInstallationTokenAndUpdateConfig();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
return withGitHubRetry(
|
||||
operation,
|
||||
token,
|
||||
errorMessage,
|
||||
additionalLogFields,
|
||||
numRetries + 1,
|
||||
);
|
||||
}
|
||||
|
||||
logger.error(errorMessage, {
|
||||
...additionalLogFields,
|
||||
...(errorFields ?? { error }),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getExistingPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branchName: string,
|
||||
githubToken: string,
|
||||
numRetries = 1,
|
||||
) {
|
||||
try {
|
||||
const octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
});
|
||||
return withGitHubRetry(
|
||||
async (token: string) => {
|
||||
const octokit = new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
|
||||
const { data: pullRequests } = await octokit.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
head: branchName,
|
||||
});
|
||||
const { data: pullRequests } = await octokit.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
head: branchName,
|
||||
});
|
||||
|
||||
if (pullRequests?.[0]) {
|
||||
return pullRequests[0];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get existing pull request`, {
|
||||
branch: branchName,
|
||||
owner,
|
||||
repo,
|
||||
...(e instanceof Error && {
|
||||
name: e.name,
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
return pullRequests?.[0] || null;
|
||||
},
|
||||
githubToken,
|
||||
"Failed to get existing pull request",
|
||||
{ branch: branchName, owner, repo },
|
||||
numRetries,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createPullRequest({
|
||||
|
|
@ -162,30 +233,33 @@ export async function getIssue({
|
|||
repo,
|
||||
issueNumber,
|
||||
githubInstallationToken,
|
||||
numRetries = 1,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
issueNumber: number;
|
||||
githubInstallationToken: string;
|
||||
numRetries?: number;
|
||||
}): Promise<GitHubIssue | null> {
|
||||
const octokit = new Octokit({
|
||||
auth: githubInstallationToken,
|
||||
});
|
||||
return withGitHubRetry(
|
||||
async (token: string) => {
|
||||
const octokit = new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: issue } = await octokit.issues.get({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const { data: issue } = await octokit.issues.get({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
return issue;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get issue`, {
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return issue;
|
||||
},
|
||||
githubInstallationToken,
|
||||
"Failed to get issue",
|
||||
undefined,
|
||||
numRetries,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getIssueComments({
|
||||
|
|
@ -194,39 +268,42 @@ export async function getIssueComments({
|
|||
issueNumber,
|
||||
githubInstallationToken,
|
||||
filterBotComments = true,
|
||||
numRetries = 1,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
issueNumber: number;
|
||||
githubInstallationToken: string;
|
||||
filterBotComments?: boolean;
|
||||
numRetries?: number;
|
||||
}): Promise<GitHubIssueComment[] | null> {
|
||||
const octokit = new Octokit({
|
||||
auth: githubInstallationToken,
|
||||
});
|
||||
return withGitHubRetry(
|
||||
async (token: string) => {
|
||||
const octokit = new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: comments } = await octokit.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const { data: comments } = await octokit.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
if (!filterBotComments) {
|
||||
return comments;
|
||||
}
|
||||
if (!filterBotComments) {
|
||||
return comments;
|
||||
}
|
||||
|
||||
return comments.filter((comment) => {
|
||||
return (
|
||||
comment.user?.type !== "Bot" || !comment.user?.name?.includes("[bot]")
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get issue comments`, {
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return comments.filter((comment) => {
|
||||
return (
|
||||
comment.user?.type !== "Bot" || !comment.user?.name?.includes("[bot]")
|
||||
);
|
||||
});
|
||||
},
|
||||
githubInstallationToken,
|
||||
"Failed to get issue comments",
|
||||
undefined,
|
||||
numRetries,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createIssue({
|
||||
|
|
@ -256,9 +333,15 @@ export async function createIssue({
|
|||
|
||||
return issue;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create issue`, {
|
||||
error,
|
||||
});
|
||||
const errorFields =
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: { error };
|
||||
logger.error(`Failed to create issue`, errorFields);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -270,6 +353,7 @@ export async function updateIssue({
|
|||
githubInstallationToken,
|
||||
body,
|
||||
title,
|
||||
numRetries = 1,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
|
|
@ -277,31 +361,33 @@ export async function updateIssue({
|
|||
githubInstallationToken: string;
|
||||
body?: string;
|
||||
title?: string;
|
||||
numRetries?: number;
|
||||
}) {
|
||||
if (!body && !title) {
|
||||
throw new Error("Must provide either body or title to update issue");
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: githubInstallationToken,
|
||||
});
|
||||
return withGitHubRetry(
|
||||
async (token: string) => {
|
||||
const octokit = new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: issue } = await octokit.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
...(body && { body }),
|
||||
...(title && { title }),
|
||||
});
|
||||
const { data: issue } = await octokit.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
...(body && { body }),
|
||||
...(title && { title }),
|
||||
});
|
||||
|
||||
return issue;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update issue`, {
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return issue;
|
||||
},
|
||||
githubInstallationToken,
|
||||
"Failed to update issue",
|
||||
undefined,
|
||||
numRetries,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createIssueComment({
|
||||
|
|
@ -310,6 +396,7 @@ export async function createIssueComment({
|
|||
issueNumber,
|
||||
body,
|
||||
githubToken,
|
||||
numRetries = 1,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
|
|
@ -320,26 +407,28 @@ export async function createIssueComment({
|
|||
* or an access token if creating a user comment.
|
||||
*/
|
||||
githubToken: string;
|
||||
numRetries?: number;
|
||||
}): Promise<GitHubIssueComment | null> {
|
||||
const octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
});
|
||||
return withGitHubRetry(
|
||||
async (token: string) => {
|
||||
const octokit = new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: comment } = await octokit.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
const { data: comment } = await octokit.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
|
||||
return comment;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create issue comment`, {
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return comment;
|
||||
},
|
||||
githubToken,
|
||||
"Failed to create issue comment",
|
||||
undefined,
|
||||
numRetries,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateIssueComment({
|
||||
|
|
@ -348,30 +437,33 @@ export async function updateIssueComment({
|
|||
commentId,
|
||||
body,
|
||||
githubInstallationToken,
|
||||
numRetries = 1,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
commentId: number;
|
||||
body: string;
|
||||
githubInstallationToken: string;
|
||||
numRetries?: number;
|
||||
}): Promise<GitHubIssueComment | null> {
|
||||
const octokit = new Octokit({
|
||||
auth: githubInstallationToken,
|
||||
});
|
||||
return withGitHubRetry(
|
||||
async (token: string) => {
|
||||
const octokit = new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: comment } = await octokit.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
body,
|
||||
});
|
||||
const { data: comment } = await octokit.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
body,
|
||||
});
|
||||
|
||||
return comment;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update issue comment`, {
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return comment;
|
||||
},
|
||||
githubInstallationToken,
|
||||
"Failed to update issue comment",
|
||||
undefined,
|
||||
numRetries,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
14
apps/open-swe/src/utils/update-config.ts
Normal file
14
apps/open-swe/src/utils/update-config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { getConfig } from "@langchain/langgraph";
|
||||
|
||||
export function updateConfig(key: string, value: unknown) {
|
||||
try {
|
||||
const config = getConfig();
|
||||
if (!config.configurable) {
|
||||
throw new Error("No configurable object found");
|
||||
}
|
||||
config.configurable[key] = value;
|
||||
} catch {
|
||||
// no-op
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,6 @@
|
|||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-tailwindcss": "^2.0.1",
|
||||
"framer-motion": "^12.4.9",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"katex": "^0.16.21",
|
||||
"langgraph-nextjs-api-passthrough": "^0.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
GITHUB_INSTALLATION_ID_COOKIE,
|
||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
GITHUB_INSTALLATION_NAME,
|
||||
GITHUB_INSTALLATION_ID,
|
||||
} from "@open-swe/shared/constants";
|
||||
import {
|
||||
getGitHubInstallationTokenOrThrow,
|
||||
|
|
@ -72,6 +73,7 @@ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } =
|
|||
[GITHUB_TOKEN_COOKIE]: getGitHubAccessTokenOrThrow(req, encryptionKey),
|
||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: installationToken,
|
||||
[GITHUB_INSTALLATION_NAME]: installationName,
|
||||
[GITHUB_INSTALLATION_ID]: installationIdCookie,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getInstallationToken } from "@/utils/github";
|
||||
import { getInstallationToken } from "@open-swe/shared/github/auth";
|
||||
import { App } from "@octokit/app";
|
||||
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
|
||||
import { encryptSecret } from "@open-swe/shared/crypto";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getInstallationToken } from "../../../../../utils/github"; // Adjusted path
|
||||
import { getInstallationToken } from "@open-swe/shared/github/auth";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
const GITHUB_API_URL = "https://api.github.com";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getInstallationToken,
|
||||
getInstallationRepositories,
|
||||
Repository,
|
||||
} from "@/utils/github";
|
||||
import { getInstallationToken } from "@open-swe/shared/github/auth";
|
||||
import { getInstallationRepositories, Repository } from "@/utils/github";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getInstallationToken } from "@/utils/github";
|
||||
import { getInstallationToken } from "@open-swe/shared/github/auth";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import * as jwt from "jsonwebtoken";
|
||||
|
||||
function getBaseApiUrl(): string {
|
||||
let baseApiUrl = new URL(
|
||||
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api",
|
||||
|
|
@ -8,54 +6,6 @@ function getBaseApiUrl(): string {
|
|||
return baseApiUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JWT for GitHub App authentication
|
||||
*/
|
||||
export function generateJWT(appId: string, privateKey: string): string {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const payload = {
|
||||
iat: now,
|
||||
exp: now + 10 * 60,
|
||||
iss: appId,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, privateKey, { algorithm: "RS256" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an installation access token for a GitHub App installation
|
||||
*/
|
||||
export async function getInstallationToken(
|
||||
installationId: string,
|
||||
appId: string,
|
||||
privateKey: string,
|
||||
): Promise<string> {
|
||||
const jwtToken = generateJWT(appId, privateKey);
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/app/installations/${installationId}/access_tokens`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
`Failed to get installation token: ${JSON.stringify(errorData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches repositories accessible to a GitHub App installation
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"@langchain/langgraph": "^0.3.8",
|
||||
"@langchain/langgraph-sdk": "^0.0.95",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const GITHUB_TOKEN_COOKIE = "x-github-access-token";
|
|||
export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token";
|
||||
export const GITHUB_INSTALLATION_NAME = "x-github-installation-name";
|
||||
export const GITHUB_PAT = "x-github-pat";
|
||||
export const GITHUB_INSTALLATION_ID = "x-github-installation-id";
|
||||
|
||||
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
|
||||
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
||||
|
|
|
|||
37
packages/shared/src/github/auth.ts
Normal file
37
packages/shared/src/github/auth.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { generateJWT } from "../jwt.js";
|
||||
|
||||
/**
|
||||
* Gets an installation access token for a GitHub App installation
|
||||
*/
|
||||
export async function getInstallationToken(
|
||||
installationId: string,
|
||||
appId: string,
|
||||
privateKey: string,
|
||||
): Promise<string> {
|
||||
const jwtToken = generateJWT(appId, privateKey);
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/app/installations/${installationId}/access_tokens`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
`Failed to get installation token: ${JSON.stringify(errorData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (typeof data !== "object" || !data || !("token" in data)) {
|
||||
throw new Error("No token returned after fetching installation token");
|
||||
}
|
||||
return data.token as string;
|
||||
}
|
||||
16
packages/shared/src/jwt.ts
Normal file
16
packages/shared/src/jwt.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import * as jwt from "jsonwebtoken";
|
||||
|
||||
/**
|
||||
* Generates a JWT for GitHub App authentication
|
||||
*/
|
||||
export function generateJWT(appId: string, privateKey: string): string {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const payload = {
|
||||
iat: now,
|
||||
exp: now + 10 * 60,
|
||||
iss: appId,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, privateKey, { algorithm: "RS256" });
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
GITHUB_USER_LOGIN_HEADER,
|
||||
GITHUB_PAT,
|
||||
DEFAULT_MCP_SERVERS,
|
||||
GITHUB_INSTALLATION_ID,
|
||||
} from "../constants.js";
|
||||
import { withLangGraph } from "@langchain/langgraph/zod";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
|
@ -417,6 +418,11 @@ export const GraphConfigurationMetadata: {
|
|||
type: "hidden",
|
||||
},
|
||||
},
|
||||
[GITHUB_INSTALLATION_ID]: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
[GITHUB_PAT]: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
|
|
@ -553,6 +559,12 @@ export const GraphConfiguration = z.object({
|
|||
[GITHUB_INSTALLATION_NAME]: withLangGraph(z.string().optional(), {
|
||||
metadata: GraphConfigurationMetadata[GITHUB_INSTALLATION_NAME],
|
||||
}),
|
||||
/**
|
||||
* The installation ID of the GitHub app the user is using to create the run.
|
||||
*/
|
||||
[GITHUB_INSTALLATION_ID]: withLangGraph(z.string().optional(), {
|
||||
metadata: GraphConfigurationMetadata[GITHUB_INSTALLATION_ID],
|
||||
}),
|
||||
/**
|
||||
* GitHub Personal Access Token. Used for simpler authentication in environments like evals
|
||||
* where GitHub App installation tokens are not available or needed.
|
||||
|
|
|
|||
|
|
@ -4194,6 +4194,7 @@ __metadata:
|
|||
eslint-plugin-import: ^2.27.5
|
||||
eslint-plugin-no-instanceof: ^1.0.1
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
jsonwebtoken: ^9.0.2
|
||||
prettier: ^3.5.2
|
||||
typescript: ~5.7.2
|
||||
typescript-eslint: ^8.22.0
|
||||
|
|
@ -4249,7 +4250,6 @@ __metadata:
|
|||
eslint-plugin-react-refresh: ^0.4.18
|
||||
framer-motion: ^12.4.9
|
||||
globals: ^15.14.0
|
||||
jsonwebtoken: ^9.0.2
|
||||
katex: ^0.16.21
|
||||
langgraph-nextjs-api-passthrough: ^0.1.3
|
||||
lodash: ^4.17.21
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue