mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
refactor: Proxy req utils, rename GITHUB_TOKEN_ENCRYPTION_KEY to SECRETS_ENCRYPTION_KEY (#391)
* refactor: Proxy req utils, rename GITHUB_TOKEN_ENCRYPTION_KEY to SECRETS_ENCRYPTION_KEY * cr
This commit is contained in:
parent
0f5bb868d9
commit
abc984409f
11 changed files with 207 additions and 208 deletions
10
README.md
10
README.md
|
|
@ -46,10 +46,10 @@ GOOGLE_API_KEY=""
|
||||||
# Daytona API key for accessing and modifying the code in the cloud sandbox.
|
# Daytona API key for accessing and modifying the code in the cloud sandbox.
|
||||||
DAYTONA_API_KEY=""
|
DAYTONA_API_KEY=""
|
||||||
|
|
||||||
# Encryption key for GitHub tokens (32-byte hex string for AES-256)
|
# Encryption key for secrets (32-byte hex string for AES-256)
|
||||||
# Should be the same value as the one used in the web app.
|
# Should be the same value as the one used in the web app.
|
||||||
# Can be generated via: `openssl rand -hex 32`
|
# Can be generated via: `openssl rand -hex 32`
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
SECRETS_ENCRYPTION_KEY=""
|
||||||
# Used for setting the git user name & email for commits.
|
# Used for setting the git user name & email for commits.
|
||||||
GITHUB_APP_NAME="open-swe-dev"
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
|
|
||||||
|
|
@ -75,13 +75,13 @@ GITHUB_APP_NAME="open-swe-dev"
|
||||||
GITHUB_APP_ID=""
|
GITHUB_APP_ID=""
|
||||||
GITHUB_APP_PRIVATE_KEY=""
|
GITHUB_APP_PRIVATE_KEY=""
|
||||||
|
|
||||||
# Encryption key for GitHub tokens (32-byte hex string for AES-256)
|
# Encryption key for secrets (32-byte hex string for AES-256)
|
||||||
# Should be the same value as the one used in the open-swe app.
|
# Should be the same value as the one used in the open-swe app.
|
||||||
# Can be generated via: `openssl rand -hex 32`
|
# Can be generated via: `openssl rand -hex 32`
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
SECRETS_ENCRYPTION_KEY=""
|
||||||
```
|
```
|
||||||
|
|
||||||
**REMINDER**: The `GITHUB_TOKEN_ENCRYPTION_KEY` environment variable must be the same in both the web and open-swe apps.
|
**REMINDER**: The `SECRETS_ENCRYPTION_KEY` environment variable must be the same in both the web and open-swe apps.
|
||||||
|
|
||||||
To get the GitHub App secrets, first create a new GitHub app (note: this is not the same as the OAuth app) in [the developer settings](https://github.com/settings/apps/new).
|
To get the GitHub App secrets, first create a new GitHub app (note: this is not the same as the OAuth app) in [the developer settings](https://github.com/settings/apps/new).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,12 @@ All headers are prefixed with `x-` to ensure they're included in LangGraph run c
|
||||||
|
|
||||||
### Token Encryption
|
### Token Encryption
|
||||||
|
|
||||||
Open SWE implements AES-256-GCM encryption for all GitHub tokens to prevent exposure in:
|
Open SWE implements AES-256-GCM encryption for all secrets passed to the LangGraph server to prevent exposure in:
|
||||||
- LangSmith trace metadata
|
- LangSmith trace metadata
|
||||||
- Run configurations
|
- Run configurations
|
||||||
- Potential unauthorized access scenarios
|
- Potential unauthorized access scenarios
|
||||||
|
|
||||||
The encryption process uses the `GITHUB_TOKEN_ENCRYPTION_KEY` environment variable and includes:
|
The encryption process uses the `SECRETS_ENCRYPTION_KEY` environment variable and includes:
|
||||||
- **Initialization Vector (IV)** for unique encryption per token
|
- **Initialization Vector (IV)** for unique encryption per token
|
||||||
- **Authentication Tag** for data integrity verification
|
- **Authentication Tag** for data integrity verification
|
||||||
- **Base64 encoding** for safe transport
|
- **Base64 encoding** for safe transport
|
||||||
|
|
@ -135,5 +135,5 @@ This design ensures tokens remain encrypted in storage and traces while being av
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
Always ensure the `GITHUB_TOKEN_ENCRYPTION_KEY` environment variable is identical between your web application and LangGraph agent deployments.
|
Always ensure the `SECRETS_ENCRYPTION_KEY` environment variable is identical between your web application and LangGraph agent deployments.
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ Before starting, ensure you have the following installed:
|
||||||
GITHUB_APP_CLIENT_SECRET=""
|
GITHUB_APP_CLIENT_SECRET=""
|
||||||
GITHUB_APP_REDIRECT_URI="http://localhost:3000/api/auth/github/callback"
|
GITHUB_APP_REDIRECT_URI="http://localhost:3000/api/auth/github/callback"
|
||||||
|
|
||||||
# Token encryption key (generate with: openssl rand -hex 32)
|
# Encryption key for secrets (generate with: openssl rand -hex 32)
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
SECRETS_ENCRYPTION_KEY=""
|
||||||
|
|
||||||
# GitHub App details (will be filled after creating GitHub App)
|
# GitHub App details (will be filled after creating GitHub App)
|
||||||
GITHUB_APP_NAME="open-swe-dev"
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
|
|
@ -101,7 +101,6 @@ Before starting, ensure you have the following installed:
|
||||||
FIRECRAWL_API_KEY="" # For URL content extraction
|
FIRECRAWL_API_KEY="" # For URL content extraction
|
||||||
|
|
||||||
# GitHub App settings (same as web app)
|
# GitHub App settings (same as web app)
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY="" # Must match web app value
|
|
||||||
GITHUB_APP_NAME="open-swe-dev"
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
GITHUB_APP_ID=""
|
GITHUB_APP_ID=""
|
||||||
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
|
@ -113,10 +112,11 @@ Before starting, ensure you have the following installed:
|
||||||
# Server configuration
|
# Server configuration
|
||||||
PORT="2024"
|
PORT="2024"
|
||||||
OPEN_SWE_APP_URL="http://localhost:3000"
|
OPEN_SWE_APP_URL="http://localhost:3000"
|
||||||
|
SECRETS_ENCRYPTION_KEY="" # Must match web app value
|
||||||
```
|
```
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
Generate the `GITHUB_TOKEN_ENCRYPTION_KEY` using: `openssl rand -hex 32`. This key must be identical in both environment files.
|
Generate the `SECRETS_ENCRYPTION_KEY` using: `openssl rand -hex 32`. This key must be identical in both environment files.
|
||||||
</Tip>
|
</Tip>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,6 @@ FIRECRAWL_API_KEY=""
|
||||||
|
|
||||||
|
|
||||||
# ------------------Github App Secrets-----------------
|
# ------------------Github App Secrets-----------------
|
||||||
# Encryption key for GitHub tokens (32-byte hex string for AES-256)
|
|
||||||
# Should be the same value as the one used in the web app, so that tokens
|
|
||||||
# encrypted in the web app can be decrypted in the agent.
|
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
|
||||||
# Used for setting the git user name & email for commits.
|
# Used for setting the git user name & email for commits.
|
||||||
# Can modify to whatever string you want.
|
# Can modify to whatever string you want.
|
||||||
GITHUB_APP_NAME="open-swe-dev"
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
|
|
@ -50,3 +46,7 @@ PORT="2024"
|
||||||
# Should be the URL of the web app. Localhost in dev, production URL
|
# Should be the URL of the web app. Localhost in dev, production URL
|
||||||
# in production.
|
# in production.
|
||||||
OPEN_SWE_APP_URL="http://localhost:3000"
|
OPEN_SWE_APP_URL="http://localhost:3000"
|
||||||
|
# Encryption key for secrets (32-byte hex string for AES-256)
|
||||||
|
# Should be the same value as the one used in the web app, so that secrets
|
||||||
|
# encrypted in the web app can be decrypted in the agent.
|
||||||
|
SECRETS_ENCRYPTION_KEY=""
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
GITHUB_USER_LOGIN_HEADER,
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
MANAGER_GRAPH_ID,
|
MANAGER_GRAPH_ID,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { encryptGitHubToken } from "@open-swe/shared/crypto";
|
import { encryptSecret } from "@open-swe/shared/crypto";
|
||||||
import { HumanMessage } from "@langchain/core/messages";
|
import { HumanMessage } from "@langchain/core/messages";
|
||||||
import {
|
import {
|
||||||
getOpenSWEAutoAcceptLabel,
|
getOpenSWEAutoAcceptLabel,
|
||||||
|
|
@ -86,10 +86,8 @@ const getHeaders = (
|
||||||
};
|
};
|
||||||
|
|
||||||
webhooks.on("issues.labeled", async ({ payload }) => {
|
webhooks.on("issues.labeled", async ({ payload }) => {
|
||||||
if (!process.env.GITHUB_TOKEN_ENCRYPTION_KEY) {
|
if (!process.env.SECRETS_ENCRYPTION_KEY) {
|
||||||
throw new Error(
|
throw new Error("SECRETS_ENCRYPTION_KEY environment variable is required");
|
||||||
"GITHUB_TOKEN_ENCRYPTION_KEY environment variable is required",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const validOpenSWELabels = [getOpenSWELabel(), getOpenSWEAutoAcceptLabel()];
|
const validOpenSWELabels = [getOpenSWELabel(), getOpenSWEAutoAcceptLabel()];
|
||||||
if (
|
if (
|
||||||
|
|
@ -132,9 +130,9 @@ webhooks.on("issues.labeled", async ({ payload }) => {
|
||||||
|
|
||||||
const langGraphClient = createLangGraphClient({
|
const langGraphClient = createLangGraphClient({
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptGitHubToken(
|
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptSecret(
|
||||||
token,
|
token,
|
||||||
process.env.GITHUB_TOKEN_ENCRYPTION_KEY,
|
process.env.SECRETS_ENCRYPTION_KEY,
|
||||||
),
|
),
|
||||||
[GITHUB_INSTALLATION_NAME]: issueData.owner,
|
[GITHUB_INSTALLATION_NAME]: issueData.owner,
|
||||||
[GITHUB_USER_ID_HEADER]: issueData.userId.toString(),
|
[GITHUB_USER_ID_HEADER]: issueData.userId.toString(),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
GITHUB_USER_ID_HEADER,
|
GITHUB_USER_ID_HEADER,
|
||||||
GITHUB_USER_LOGIN_HEADER,
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { decryptGitHubToken } from "@open-swe/shared/crypto";
|
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||||
import { verifyGitHubWebhookOrThrow } from "./github.js";
|
import { verifyGitHubWebhookOrThrow } from "./github.js";
|
||||||
import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js";
|
import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js";
|
||||||
|
|
||||||
|
|
@ -49,11 +49,9 @@ export const auth = new Auth()
|
||||||
return await verifyGitHubWebhookOrThrow(request);
|
return await verifyGitHubWebhookOrThrow(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
throw new Error(
|
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
||||||
"Missing GITHUB_TOKEN_ENCRYPTION_KEY environment variable.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const installationNameHeader = request.headers.get(
|
const installationNameHeader = request.headers.get(
|
||||||
|
|
@ -90,14 +88,14 @@ export const auth = new Auth()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
user = await verifyGithubUserId(
|
user = await verifyGithubUserId(
|
||||||
decryptGitHubToken(encryptedInstallationToken, encryptionKey),
|
decryptSecret(encryptedInstallationToken, encryptionKey),
|
||||||
Number(userIdHeader),
|
Number(userIdHeader),
|
||||||
userLoginHeader,
|
userLoginHeader,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Ensure we decrypt the token before passing to the verification function.
|
// Ensure we decrypt the token before passing to the verification function.
|
||||||
user = await verifyGithubUser(
|
user = await verifyGithubUser(
|
||||||
decryptGitHubToken(encryptedAccessToken, encryptionKey),
|
decryptSecret(encryptedAccessToken, encryptionKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { decryptGitHubToken } from "@open-swe/shared/crypto";
|
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||||
|
|
||||||
export function getGitHubTokensFromConfig(config: GraphConfig): {
|
export function getGitHubTokensFromConfig(config: GraphConfig): {
|
||||||
githubAccessToken: string;
|
githubAccessToken: string;
|
||||||
|
|
@ -22,18 +22,16 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the encryption key from environment variables
|
// Get the encryption key from environment variables
|
||||||
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
throw new Error(
|
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
||||||
"Missing GITHUB_TOKEN_ENCRYPTION_KEY environment variable.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the GitHub token
|
// Decrypt the GitHub token
|
||||||
const githubAccessToken = encryptedGitHubToken
|
const githubAccessToken = encryptedGitHubToken
|
||||||
? decryptGitHubToken(encryptedGitHubToken, encryptionKey)
|
? decryptSecret(encryptedGitHubToken, encryptionKey)
|
||||||
: "";
|
: "";
|
||||||
const githubInstallationToken = decryptGitHubToken(
|
const githubInstallationToken = decryptSecret(
|
||||||
encryptedInstallationToken,
|
encryptedInstallationToken,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,6 @@ GITHUB_APP_CLIENT_SECRET=""
|
||||||
# your GitHub app settings.
|
# your GitHub app settings.
|
||||||
GITHUB_APP_REDIRECT_URI="http://localhost:3000/api/auth/github/callback"
|
GITHUB_APP_REDIRECT_URI="http://localhost:3000/api/auth/github/callback"
|
||||||
|
|
||||||
# Encryption key for GitHub tokens (32-byte hex string for AES-256)
|
|
||||||
# Should be the same value as the one used in the web app, so that tokens
|
|
||||||
# encrypted in the web app can be decrypted in the agent.
|
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
|
||||||
# Used for setting the git user name & email for commits.
|
# Used for setting the git user name & email for commits.
|
||||||
# Can modify to whatever string you want.
|
# Can modify to whatever string you want.
|
||||||
GITHUB_APP_NAME="open-swe-dev"
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
|
|
@ -29,3 +25,7 @@ NEXT_PUBLIC_API_URL="http://localhost:3000/api"
|
||||||
# The API URL of the LangGraph server. Used in the proxy route to forward
|
# The API URL of the LangGraph server. Used in the proxy route to forward
|
||||||
# requests to the LangGraph server.
|
# requests to the LangGraph server.
|
||||||
LANGGRAPH_API_URL="http://localhost:2024"
|
LANGGRAPH_API_URL="http://localhost:2024"
|
||||||
|
# Encryption key for secrets (32-byte hex string for AES-256)
|
||||||
|
# Should be the same value as the one used in the web app, so that secrets
|
||||||
|
# encrypted in the web app can be decrypted in the agent.
|
||||||
|
SECRETS_ENCRYPTION_KEY=""
|
||||||
|
|
|
||||||
|
|
@ -5,150 +5,11 @@ import {
|
||||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||||
GITHUB_INSTALLATION_NAME,
|
GITHUB_INSTALLATION_NAME,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { encryptGitHubToken } from "@open-swe/shared/crypto";
|
import {
|
||||||
import { NextRequest } from "next/server";
|
getGitHubInstallationTokenOrThrow,
|
||||||
import { getInstallationToken } from "@/utils/github";
|
getInstallationNameFromReq,
|
||||||
import { App } from "@octokit/app";
|
getGitHubAccessTokenOrThrow,
|
||||||
import { validate } from "uuid";
|
} from "./utils";
|
||||||
|
|
||||||
function getGitHubAccessTokenOrThrow(
|
|
||||||
req: NextRequest,
|
|
||||||
encryptionKey: string,
|
|
||||||
): string {
|
|
||||||
const token = req.cookies.get(GITHUB_TOKEN_COOKIE)?.value ?? "";
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
throw new Error(
|
|
||||||
"No GitHub access token found. User must authenticate first.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return encryptGitHubToken(token, encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGitHubInstallationTokenOrThrow(
|
|
||||||
installationIdCookie: string,
|
|
||||||
encryptionKey: string,
|
|
||||||
): Promise<string> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await getInstallationToken(
|
|
||||||
installationIdCookie,
|
|
||||||
appId,
|
|
||||||
privateAppKey,
|
|
||||||
);
|
|
||||||
return encryptGitHubToken(token, encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getInstallationName(installationId: string) {
|
|
||||||
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_APP_PRIVATE_KEY) {
|
|
||||||
throw new Error("GitHub App ID or Private App Key is not configured.");
|
|
||||||
}
|
|
||||||
const app = new App({
|
|
||||||
appId: process.env.GITHUB_APP_ID,
|
|
||||||
privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get installation details
|
|
||||||
const { data } = await app.octokit.request(
|
|
||||||
"GET /app/installations/{installation_id}",
|
|
||||||
{
|
|
||||||
installation_id: Number(installationId),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const installationName =
|
|
||||||
data.account && "name" in data.account
|
|
||||||
? data.account.name
|
|
||||||
: data.account?.login;
|
|
||||||
|
|
||||||
return installationName ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNewRunRequest = (reqUrlStr: string, reqMethod: string) => {
|
|
||||||
try {
|
|
||||||
const reqPathnameParts = new URL(reqUrlStr).pathname.split("/");
|
|
||||||
const isCreateNewRunReq =
|
|
||||||
reqPathnameParts?.[1] === "api" &&
|
|
||||||
reqPathnameParts?.[2] === "threads" &&
|
|
||||||
validate(reqPathnameParts?.[3]) &&
|
|
||||||
reqPathnameParts?.[4] === "runs" &&
|
|
||||||
reqMethod.toLowerCase() === "post";
|
|
||||||
const isStreamRunReq =
|
|
||||||
reqPathnameParts?.[1] === "api" &&
|
|
||||||
reqPathnameParts?.[2] === "threads" &&
|
|
||||||
validate(reqPathnameParts?.[3]) &&
|
|
||||||
reqPathnameParts?.[4] === "runs" &&
|
|
||||||
validate(reqPathnameParts?.[5]) &&
|
|
||||||
reqPathnameParts?.[6]?.startsWith("stream") &&
|
|
||||||
reqMethod.toLowerCase() === "get";
|
|
||||||
return isCreateNewRunReq || isStreamRunReq;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isGetStateRequest = (reqUrlStr: string, reqMethod: string) => {
|
|
||||||
try {
|
|
||||||
const reqPathnameParts = new URL(reqUrlStr).pathname.split("/");
|
|
||||||
const isGetStateReq =
|
|
||||||
reqPathnameParts?.[1] === "api" &&
|
|
||||||
reqPathnameParts?.[2] === "threads" &&
|
|
||||||
validate(reqPathnameParts?.[3]) &&
|
|
||||||
reqPathnameParts?.[4] === "state" &&
|
|
||||||
reqMethod.toLowerCase() === "get";
|
|
||||||
return isGetStateReq;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSearchThreadsRequest = (reqUrlStr: string, reqMethod: string) => {
|
|
||||||
try {
|
|
||||||
const reqPathnameParts = new URL(reqUrlStr).pathname.split("/");
|
|
||||||
const isGetStateReq =
|
|
||||||
reqPathnameParts?.[1] === "api" &&
|
|
||||||
reqPathnameParts?.[2] === "threads" &&
|
|
||||||
reqPathnameParts?.[3] === "search" &&
|
|
||||||
reqMethod.toLowerCase() === "post";
|
|
||||||
return isGetStateReq;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getInstallationNameFromReq(
|
|
||||||
req: Request,
|
|
||||||
installationId: string,
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const requestJson = await req.json();
|
|
||||||
const installationName = requestJson?.input?.targetRepository?.owner;
|
|
||||||
if (installationName) {
|
|
||||||
return installationName;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
isNewRunRequest(req.url, req.method) ||
|
|
||||||
isGetStateRequest(req.url, req.method) ||
|
|
||||||
isSearchThreadsRequest(req.url, req.method)
|
|
||||||
) {
|
|
||||||
return await getInstallationName(installationId);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This file acts as a proxy for requests to your LangGraph server.
|
// This file acts as a proxy for requests to your LangGraph server.
|
||||||
// Read the [Going to Production](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#going-to-production) section for more information.
|
// Read the [Going to Production](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#going-to-production) section for more information.
|
||||||
|
|
@ -159,10 +20,10 @@ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } =
|
||||||
runtime: "edge", // default
|
runtime: "edge", // default
|
||||||
disableWarningLog: true,
|
disableWarningLog: true,
|
||||||
headers: async (req) => {
|
headers: async (req) => {
|
||||||
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"GITHUB_TOKEN_ENCRYPTION_KEY environment variable is required",
|
"SECRETS_ENCRYPTION_KEY environment variable is required",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const installationIdCookie = req.cookies.get(
|
const installationIdCookie = req.cookies.get(
|
||||||
|
|
|
||||||
145
apps/web/src/app/api/[..._path]/utils.ts
Normal file
145
apps/web/src/app/api/[..._path]/utils.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { getInstallationToken } from "@/utils/github";
|
||||||
|
import { App } from "@octokit/app";
|
||||||
|
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
|
||||||
|
import { encryptSecret } from "@open-swe/shared/crypto";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { validate } from "uuid";
|
||||||
|
|
||||||
|
export function getGitHubAccessTokenOrThrow(
|
||||||
|
req: NextRequest,
|
||||||
|
encryptionKey: string,
|
||||||
|
): string {
|
||||||
|
const token = req.cookies.get(GITHUB_TOKEN_COOKIE)?.value ?? "";
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"No GitHub access token found. User must authenticate first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encryptSecret(token, encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGitHubInstallationTokenOrThrow(
|
||||||
|
installationIdCookie: string,
|
||||||
|
encryptionKey: string,
|
||||||
|
): Promise<string> {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getInstallationToken(
|
||||||
|
installationIdCookie,
|
||||||
|
appId,
|
||||||
|
privateAppKey,
|
||||||
|
);
|
||||||
|
return encryptSecret(token, encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInstallationName(installationId: string) {
|
||||||
|
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_APP_PRIVATE_KEY) {
|
||||||
|
throw new Error("GitHub App ID or Private App Key is not configured.");
|
||||||
|
}
|
||||||
|
const app = new App({
|
||||||
|
appId: process.env.GITHUB_APP_ID,
|
||||||
|
privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get installation details
|
||||||
|
const { data } = await app.octokit.request(
|
||||||
|
"GET /app/installations/{installation_id}",
|
||||||
|
{
|
||||||
|
installation_id: Number(installationId),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const installationName =
|
||||||
|
data.account && "name" in data.account
|
||||||
|
? data.account.name
|
||||||
|
: data.account?.login;
|
||||||
|
|
||||||
|
return installationName ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNewRunRequest(reqUrlStr: string, reqMethod: string) {
|
||||||
|
try {
|
||||||
|
const reqPathnameParts = new URL(reqUrlStr).pathname.split("/");
|
||||||
|
const isCreateNewRunReq =
|
||||||
|
reqPathnameParts?.[1] === "api" &&
|
||||||
|
reqPathnameParts?.[2] === "threads" &&
|
||||||
|
validate(reqPathnameParts?.[3]) &&
|
||||||
|
reqPathnameParts?.[4] === "runs" &&
|
||||||
|
reqMethod.toLowerCase() === "post";
|
||||||
|
const isStreamRunReq =
|
||||||
|
reqPathnameParts?.[1] === "api" &&
|
||||||
|
reqPathnameParts?.[2] === "threads" &&
|
||||||
|
validate(reqPathnameParts?.[3]) &&
|
||||||
|
reqPathnameParts?.[4] === "runs" &&
|
||||||
|
validate(reqPathnameParts?.[5]) &&
|
||||||
|
reqPathnameParts?.[6]?.startsWith("stream") &&
|
||||||
|
reqMethod.toLowerCase() === "get";
|
||||||
|
return isCreateNewRunReq || isStreamRunReq;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGetStateRequest(reqUrlStr: string, reqMethod: string) {
|
||||||
|
try {
|
||||||
|
const reqPathnameParts = new URL(reqUrlStr).pathname.split("/");
|
||||||
|
const isGetStateReq =
|
||||||
|
reqPathnameParts?.[1] === "api" &&
|
||||||
|
reqPathnameParts?.[2] === "threads" &&
|
||||||
|
validate(reqPathnameParts?.[3]) &&
|
||||||
|
reqPathnameParts?.[4] === "state" &&
|
||||||
|
reqMethod.toLowerCase() === "get";
|
||||||
|
return isGetStateReq;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSearchThreadsRequest(reqUrlStr: string, reqMethod: string) {
|
||||||
|
try {
|
||||||
|
const reqPathnameParts = new URL(reqUrlStr).pathname.split("/");
|
||||||
|
const isGetStateReq =
|
||||||
|
reqPathnameParts?.[1] === "api" &&
|
||||||
|
reqPathnameParts?.[2] === "threads" &&
|
||||||
|
reqPathnameParts?.[3] === "search" &&
|
||||||
|
reqMethod.toLowerCase() === "post";
|
||||||
|
return isGetStateReq;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInstallationNameFromReq(
|
||||||
|
req: Request,
|
||||||
|
installationId: string,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const requestJson = await req.json();
|
||||||
|
const installationName = requestJson?.input?.targetRepository?.owner;
|
||||||
|
if (installationName) {
|
||||||
|
return installationName;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
isNewRunRequest(req.url, req.method) ||
|
||||||
|
isGetStateRequest(req.url, req.method) ||
|
||||||
|
isSearchThreadsRequest(req.url, req.method)
|
||||||
|
) {
|
||||||
|
return await getInstallationName(installationId);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,19 +20,16 @@ function deriveKey(encryptionKey: string): Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts a GitHub token using AES-256-GCM
|
* Encrypts a secret using AES-256-GCM
|
||||||
*
|
*
|
||||||
* @param token - The GitHub access token to encrypt
|
* @param secret - The secret to encrypt
|
||||||
* @param encryptionKey - The encryption key (will be hashed to 256 bits)
|
* @param encryptionKey - The encryption key (will be hashed to 256 bits)
|
||||||
* @returns Base64 encoded encrypted data containing IV, encrypted token, and auth tag
|
* @returns Base64 encoded encrypted data containing IV, encrypted token, and auth tag
|
||||||
* @throws Error if encryption fails or inputs are invalid
|
* @throws Error if encryption fails or inputs are invalid
|
||||||
*/
|
*/
|
||||||
export function encryptGitHubToken(
|
export function encryptSecret(secret: string, encryptionKey: string): string {
|
||||||
token: string,
|
if (!secret || typeof secret !== "string") {
|
||||||
encryptionKey: string,
|
throw new Error("Secret must be a non-empty string");
|
||||||
): string {
|
|
||||||
if (!token || typeof token !== "string") {
|
|
||||||
throw new Error("Token must be a non-empty string");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encryptionKey || typeof encryptionKey !== "string") {
|
if (!encryptionKey || typeof encryptionKey !== "string") {
|
||||||
|
|
@ -49,9 +46,9 @@ export function encryptGitHubToken(
|
||||||
// Create cipher
|
// Create cipher
|
||||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||||
|
|
||||||
// Encrypt the token
|
// Encrypt the secret
|
||||||
const encryptedBuffer = Buffer.concat([
|
const encryptedBuffer = Buffer.concat([
|
||||||
cipher.update(token, "utf8"),
|
cipher.update(secret, "utf8"),
|
||||||
cipher.final(),
|
cipher.final(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -64,25 +61,25 @@ export function encryptGitHubToken(
|
||||||
return combined.toString("base64");
|
return combined.toString("base64");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to encrypt token: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`Failed to encrypt secret: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypts a GitHub token using AES-256-GCM
|
* Decrypts a secret using AES-256-GCM
|
||||||
*
|
*
|
||||||
* @param encryptedToken - Base64 encoded encrypted data from encryptGitHubToken
|
* @param encryptedSecret - Base64 encoded encrypted data from encryptSecret
|
||||||
* @param encryptionKey - The encryption key used for encryption
|
* @param encryptionKey - The encryption key used for encryption
|
||||||
* @returns The decrypted GitHub access token
|
* @returns The decrypted secret
|
||||||
* @throws Error if decryption fails or inputs are invalid
|
* @throws Error if decryption fails or inputs are invalid
|
||||||
*/
|
*/
|
||||||
export function decryptGitHubToken(
|
export function decryptSecret(
|
||||||
encryptedToken: string,
|
encryptedSecret: string,
|
||||||
encryptionKey: string,
|
encryptionKey: string,
|
||||||
): string {
|
): string {
|
||||||
if (!encryptedToken || typeof encryptedToken !== "string") {
|
if (!encryptedSecret || typeof encryptedSecret !== "string") {
|
||||||
throw new Error("Encrypted token must be a non-empty string");
|
throw new Error("Encrypted secret must be a non-empty string");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encryptionKey || typeof encryptionKey !== "string") {
|
if (!encryptionKey || typeof encryptionKey !== "string") {
|
||||||
|
|
@ -91,11 +88,13 @@ export function decryptGitHubToken(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decode the combined data
|
// Decode the combined data
|
||||||
const combined = Buffer.from(encryptedToken, "base64");
|
const combined = Buffer.from(encryptedSecret, "base64");
|
||||||
|
|
||||||
// Minimum length: IV_LENGTH + TAG_LENGTH + 1 byte for data
|
// Minimum length: IV_LENGTH + TAG_LENGTH + 1 byte for data
|
||||||
if (combined.length < IV_LENGTH + TAG_LENGTH + 1) {
|
if (combined.length < IV_LENGTH + TAG_LENGTH + 1) {
|
||||||
throw new Error("Invalid encrypted token format: too short or malformed");
|
throw new Error(
|
||||||
|
"Invalid encrypted secret format: too short or malformed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract IV, encrypted data, and tag
|
// Extract IV, encrypted data, and tag
|
||||||
|
|
@ -126,7 +125,7 @@ export function decryptGitHubToken(
|
||||||
return decryptedBuffer.toString("utf8");
|
return decryptedBuffer.toString("utf8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to decrypt token: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`Failed to decrypt secret: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue