mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 05:43:14 +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=""
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
GITHUB_APP_NAME="open-swe-dev"
|
||||
|
||||
|
|
@ -75,13 +75,13 @@ GITHUB_APP_NAME="open-swe-dev"
|
|||
GITHUB_APP_ID=""
|
||||
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.
|
||||
# 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).
|
||||
|
||||
|
|
|
|||
|
|
@ -47,12 +47,12 @@ All headers are prefixed with `x-` to ensure they're included in LangGraph run c
|
|||
|
||||
### 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
|
||||
- Run configurations
|
||||
- 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
|
||||
- **Authentication Tag** for data integrity verification
|
||||
- **Base64 encoding** for safe transport
|
||||
|
|
@ -135,5 +135,5 @@ This design ensures tokens remain encrypted in storage and traces while being av
|
|||
</Note>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ Before starting, ensure you have the following installed:
|
|||
GITHUB_APP_CLIENT_SECRET=""
|
||||
GITHUB_APP_REDIRECT_URI="http://localhost:3000/api/auth/github/callback"
|
||||
|
||||
# Token encryption key (generate with: openssl rand -hex 32)
|
||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
||||
# Encryption key for secrets (generate with: openssl rand -hex 32)
|
||||
SECRETS_ENCRYPTION_KEY=""
|
||||
|
||||
# GitHub App details (will be filled after creating GitHub App)
|
||||
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
|
||||
|
||||
# GitHub App settings (same as web app)
|
||||
GITHUB_TOKEN_ENCRYPTION_KEY="" # Must match web app value
|
||||
GITHUB_APP_NAME="open-swe-dev"
|
||||
GITHUB_APP_ID=""
|
||||
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||
|
|
@ -113,10 +112,11 @@ Before starting, ensure you have the following installed:
|
|||
# Server configuration
|
||||
PORT="2024"
|
||||
OPEN_SWE_APP_URL="http://localhost:3000"
|
||||
SECRETS_ENCRYPTION_KEY="" # Must match web app value
|
||||
```
|
||||
|
||||
<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>
|
||||
</Step>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ FIRECRAWL_API_KEY=""
|
|||
|
||||
|
||||
# ------------------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.
|
||||
# Can modify to whatever string you want.
|
||||
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
|
||||
# in production.
|
||||
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,
|
||||
MANAGER_GRAPH_ID,
|
||||
} 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 {
|
||||
getOpenSWEAutoAcceptLabel,
|
||||
|
|
@ -86,10 +86,8 @@ const getHeaders = (
|
|||
};
|
||||
|
||||
webhooks.on("issues.labeled", async ({ payload }) => {
|
||||
if (!process.env.GITHUB_TOKEN_ENCRYPTION_KEY) {
|
||||
throw new Error(
|
||||
"GITHUB_TOKEN_ENCRYPTION_KEY environment variable is required",
|
||||
);
|
||||
if (!process.env.SECRETS_ENCRYPTION_KEY) {
|
||||
throw new Error("SECRETS_ENCRYPTION_KEY environment variable is required");
|
||||
}
|
||||
const validOpenSWELabels = [getOpenSWELabel(), getOpenSWEAutoAcceptLabel()];
|
||||
if (
|
||||
|
|
@ -132,9 +130,9 @@ webhooks.on("issues.labeled", async ({ payload }) => {
|
|||
|
||||
const langGraphClient = createLangGraphClient({
|
||||
defaultHeaders: {
|
||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptGitHubToken(
|
||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptSecret(
|
||||
token,
|
||||
process.env.GITHUB_TOKEN_ENCRYPTION_KEY,
|
||||
process.env.SECRETS_ENCRYPTION_KEY,
|
||||
),
|
||||
[GITHUB_INSTALLATION_NAME]: issueData.owner,
|
||||
[GITHUB_USER_ID_HEADER]: issueData.userId.toString(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
GITHUB_USER_ID_HEADER,
|
||||
GITHUB_USER_LOGIN_HEADER,
|
||||
} 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 { createWithOwnerMetadata, createOwnerFilter } from "./utils.js";
|
||||
|
||||
|
|
@ -49,11 +49,9 @@ export const auth = new Auth()
|
|||
return await verifyGitHubWebhookOrThrow(request);
|
||||
}
|
||||
|
||||
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!encryptionKey) {
|
||||
throw new Error(
|
||||
"Missing GITHUB_TOKEN_ENCRYPTION_KEY environment variable.",
|
||||
);
|
||||
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
||||
}
|
||||
|
||||
const installationNameHeader = request.headers.get(
|
||||
|
|
@ -90,14 +88,14 @@ export const auth = new Auth()
|
|||
});
|
||||
}
|
||||
user = await verifyGithubUserId(
|
||||
decryptGitHubToken(encryptedInstallationToken, encryptionKey),
|
||||
decryptSecret(encryptedInstallationToken, encryptionKey),
|
||||
Number(userIdHeader),
|
||||
userLoginHeader,
|
||||
);
|
||||
} else {
|
||||
// Ensure we decrypt the token before passing to the verification function.
|
||||
user = await verifyGithubUser(
|
||||
decryptGitHubToken(encryptedAccessToken, encryptionKey),
|
||||
decryptSecret(encryptedAccessToken, encryptionKey),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
} from "@open-swe/shared/constants";
|
||||
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): {
|
||||
githubAccessToken: string;
|
||||
|
|
@ -22,18 +22,16 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
|
|||
}
|
||||
|
||||
// Get the encryption key from environment variables
|
||||
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!encryptionKey) {
|
||||
throw new Error(
|
||||
"Missing GITHUB_TOKEN_ENCRYPTION_KEY environment variable.",
|
||||
);
|
||||
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
||||
}
|
||||
|
||||
// Decrypt the GitHub token
|
||||
const githubAccessToken = encryptedGitHubToken
|
||||
? decryptGitHubToken(encryptedGitHubToken, encryptionKey)
|
||||
? decryptSecret(encryptedGitHubToken, encryptionKey)
|
||||
: "";
|
||||
const githubInstallationToken = decryptGitHubToken(
|
||||
const githubInstallationToken = decryptSecret(
|
||||
encryptedInstallationToken,
|
||||
encryptionKey,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ GITHUB_APP_CLIENT_SECRET=""
|
|||
# your GitHub app settings.
|
||||
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.
|
||||
# Can modify to whatever string you want.
|
||||
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
|
||||
# requests to the LangGraph server.
|
||||
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_NAME,
|
||||
} from "@open-swe/shared/constants";
|
||||
import { encryptGitHubToken } from "@open-swe/shared/crypto";
|
||||
import { NextRequest } from "next/server";
|
||||
import { getInstallationToken } from "@/utils/github";
|
||||
import { App } from "@octokit/app";
|
||||
import { validate } from "uuid";
|
||||
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
import {
|
||||
getGitHubInstallationTokenOrThrow,
|
||||
getInstallationNameFromReq,
|
||||
getGitHubAccessTokenOrThrow,
|
||||
} from "./utils";
|
||||
|
||||
// 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.
|
||||
|
|
@ -159,10 +20,10 @@ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } =
|
|||
runtime: "edge", // default
|
||||
disableWarningLog: true,
|
||||
headers: async (req) => {
|
||||
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||
if (!encryptionKey) {
|
||||
throw new Error(
|
||||
"GITHUB_TOKEN_ENCRYPTION_KEY environment variable is required",
|
||||
"SECRETS_ENCRYPTION_KEY environment variable is required",
|
||||
);
|
||||
}
|
||||
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)
|
||||
* @returns Base64 encoded encrypted data containing IV, encrypted token, and auth tag
|
||||
* @throws Error if encryption fails or inputs are invalid
|
||||
*/
|
||||
export function encryptGitHubToken(
|
||||
token: string,
|
||||
encryptionKey: string,
|
||||
): string {
|
||||
if (!token || typeof token !== "string") {
|
||||
throw new Error("Token must be a non-empty string");
|
||||
export function encryptSecret(secret: string, encryptionKey: string): string {
|
||||
if (!secret || typeof secret !== "string") {
|
||||
throw new Error("Secret must be a non-empty string");
|
||||
}
|
||||
|
||||
if (!encryptionKey || typeof encryptionKey !== "string") {
|
||||
|
|
@ -49,9 +46,9 @@ export function encryptGitHubToken(
|
|||
// Create cipher
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
// Encrypt the token
|
||||
// Encrypt the secret
|
||||
const encryptedBuffer = Buffer.concat([
|
||||
cipher.update(token, "utf8"),
|
||||
cipher.update(secret, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
|
||||
|
|
@ -64,25 +61,25 @@ export function encryptGitHubToken(
|
|||
return combined.toString("base64");
|
||||
} catch (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
|
||||
* @returns The decrypted GitHub access token
|
||||
* @returns The decrypted secret
|
||||
* @throws Error if decryption fails or inputs are invalid
|
||||
*/
|
||||
export function decryptGitHubToken(
|
||||
encryptedToken: string,
|
||||
export function decryptSecret(
|
||||
encryptedSecret: string,
|
||||
encryptionKey: string,
|
||||
): string {
|
||||
if (!encryptedToken || typeof encryptedToken !== "string") {
|
||||
throw new Error("Encrypted token must be a non-empty string");
|
||||
if (!encryptedSecret || typeof encryptedSecret !== "string") {
|
||||
throw new Error("Encrypted secret must be a non-empty string");
|
||||
}
|
||||
|
||||
if (!encryptionKey || typeof encryptionKey !== "string") {
|
||||
|
|
@ -91,11 +88,13 @@ export function decryptGitHubToken(
|
|||
|
||||
try {
|
||||
// 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
|
||||
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
|
||||
|
|
@ -126,7 +125,7 @@ export function decryptGitHubToken(
|
|||
return decryptedBuffer.toString("utf8");
|
||||
} catch (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