From 2e4c43c1cf6a14c6b2d1d502b70337fae35bc1ce Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 6 Apr 2026 12:17:29 -0400 Subject: [PATCH] refactor: replace Bun.serve with Node http.createServer in OAuth handlers (#18327) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- packages/opencode/src/mcp/oauth-callback.ts | 167 ++++++++++---------- packages/opencode/src/plugin/codex.ts | 119 +++++++------- 2 files changed, 147 insertions(+), 139 deletions(-) diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index 3a1ca54044..dd1d886fc1 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -1,4 +1,5 @@ import { createConnection } from "net" +import { createServer } from "http" import { Log } from "../util/log" import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider" @@ -52,7 +53,7 @@ interface PendingAuth { } export namespace McpOAuthCallback { - let server: ReturnType | undefined + let server: ReturnType | undefined const pendingAuths = new Map() // Reverse index: mcpName → oauthState, so cancelPending(mcpName) can // find the right entry in pendingAuths (which is keyed by oauthState). @@ -60,6 +61,80 @@ export namespace McpOAuthCallback { const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes + function cleanupStateIndex(oauthState: string) { + for (const [name, state] of mcpNameToState) { + if (state === oauthState) { + mcpNameToState.delete(name) + break + } + } + } + + function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) { + const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`) + + if (url.pathname !== OAUTH_CALLBACK_PATH) { + res.writeHead(404) + res.end("Not found") + return + } + + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const error = url.searchParams.get("error") + const errorDescription = url.searchParams.get("error_description") + + log.info("received oauth callback", { hasCode: !!code, state, error }) + + // Enforce state parameter presence + if (!state) { + const errorMsg = "Missing required state parameter - potential CSRF attack" + log.error("oauth callback missing state parameter", { url: url.toString() }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + if (error) { + const errorMsg = errorDescription || error + if (pendingAuths.has(state)) { + const pending = pendingAuths.get(state)! + clearTimeout(pending.timeout) + pendingAuths.delete(state) + cleanupStateIndex(state) + pending.reject(new Error(errorMsg)) + } + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + if (!code) { + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR("No authorization code provided")) + return + } + + // Validate state parameter + if (!pendingAuths.has(state)) { + const errorMsg = "Invalid or expired state parameter - potential CSRF attack" + log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + const pending = pendingAuths.get(state)! + + clearTimeout(pending.timeout) + pendingAuths.delete(state) + cleanupStateIndex(state) + pending.resolve(code) + + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_SUCCESS) + } + export async function ensureRunning(): Promise { if (server) return @@ -69,88 +144,14 @@ export namespace McpOAuthCallback { return } - server = Bun.serve({ - port: OAUTH_CALLBACK_PORT, - fetch(req) { - const url = new URL(req.url) - - if (url.pathname !== OAUTH_CALLBACK_PATH) { - return new Response("Not found", { status: 404 }) - } - - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - const error = url.searchParams.get("error") - const errorDescription = url.searchParams.get("error_description") - - log.info("received oauth callback", { hasCode: !!code, state, error }) - - // Enforce state parameter presence - if (!state) { - const errorMsg = "Missing required state parameter - potential CSRF attack" - log.error("oauth callback missing state parameter", { url: url.toString() }) - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - if (error) { - const errorMsg = errorDescription || error - if (pendingAuths.has(state)) { - const pending = pendingAuths.get(state)! - clearTimeout(pending.timeout) - pendingAuths.delete(state) - for (const [name, s] of mcpNameToState) { - if (s === state) { - mcpNameToState.delete(name) - break - } - } - pending.reject(new Error(errorMsg)) - } - return new Response(HTML_ERROR(errorMsg), { - headers: { "Content-Type": "text/html" }, - }) - } - - if (!code) { - return new Response(HTML_ERROR("No authorization code provided"), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - // Validate state parameter - if (!pendingAuths.has(state)) { - const errorMsg = "Invalid or expired state parameter - potential CSRF attack" - log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) }) - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - const pending = pendingAuths.get(state)! - - clearTimeout(pending.timeout) - pendingAuths.delete(state) - // Clean up reverse index - for (const [name, s] of mcpNameToState) { - if (s === state) { - mcpNameToState.delete(name) - break - } - } - pending.resolve(code) - - return new Response(HTML_SUCCESS, { - headers: { "Content-Type": "text/html" }, - }) - }, + server = createServer(handleRequest) + await new Promise((resolve, reject) => { + server!.listen(OAUTH_CALLBACK_PORT, () => { + log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT }) + resolve() + }) + server!.on("error", reject) }) - - log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT }) } export function waitForCallback(oauthState: string, mcpName?: string): Promise { @@ -196,7 +197,7 @@ export namespace McpOAuthCallback { export async function stop(): Promise { if (server) { - server.stop() + await new Promise((resolve) => server!.close(() => resolve())) server = undefined log.info("oauth callback server stopped") } diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index ee42b95171..77c00eb4f0 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -6,6 +6,7 @@ import os from "os" import { ProviderTransform } from "@/provider/transform" import { ModelID, ProviderID } from "@/provider/schema" import { setTimeout as sleep } from "node:timers/promises" +import { createServer } from "http" const log = Log.create({ service: "plugin.codex" }) @@ -241,7 +242,7 @@ interface PendingOAuth { reject: (error: Error) => void } -let oauthServer: ReturnType | undefined +let oauthServer: ReturnType | undefined let pendingOAuth: PendingOAuth | undefined async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> { @@ -249,77 +250,83 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string } return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } } - oauthServer = Bun.serve({ - port: OAUTH_PORT, - fetch(req) { - const url = new URL(req.url) + oauthServer = createServer((req, res) => { + const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`) - if (url.pathname === "/auth/callback") { - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - const error = url.searchParams.get("error") - const errorDescription = url.searchParams.get("error_description") + if (url.pathname === "/auth/callback") { + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const error = url.searchParams.get("error") + const errorDescription = url.searchParams.get("error_description") - if (error) { - const errorMsg = errorDescription || error - pendingOAuth?.reject(new Error(errorMsg)) - pendingOAuth = undefined - return new Response(HTML_ERROR(errorMsg), { - headers: { "Content-Type": "text/html" }, - }) - } - - if (!code) { - const errorMsg = "Missing authorization code" - pendingOAuth?.reject(new Error(errorMsg)) - pendingOAuth = undefined - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - if (!pendingOAuth || state !== pendingOAuth.state) { - const errorMsg = "Invalid state - potential CSRF attack" - pendingOAuth?.reject(new Error(errorMsg)) - pendingOAuth = undefined - return new Response(HTML_ERROR(errorMsg), { - status: 400, - headers: { "Content-Type": "text/html" }, - }) - } - - const current = pendingOAuth + if (error) { + const errorMsg = errorDescription || error + pendingOAuth?.reject(new Error(errorMsg)) pendingOAuth = undefined - - exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce) - .then((tokens) => current.resolve(tokens)) - .catch((err) => current.reject(err)) - - return new Response(HTML_SUCCESS, { - headers: { "Content-Type": "text/html" }, - }) + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return } - if (url.pathname === "/cancel") { - pendingOAuth?.reject(new Error("Login cancelled")) + if (!code) { + const errorMsg = "Missing authorization code" + pendingOAuth?.reject(new Error(errorMsg)) pendingOAuth = undefined - return new Response("Login cancelled", { status: 200 }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return } - return new Response("Not found", { status: 404 }) - }, + if (!pendingOAuth || state !== pendingOAuth.state) { + const errorMsg = "Invalid state - potential CSRF attack" + pendingOAuth?.reject(new Error(errorMsg)) + pendingOAuth = undefined + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + const current = pendingOAuth + pendingOAuth = undefined + + exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce) + .then((tokens) => current.resolve(tokens)) + .catch((err) => current.reject(err)) + + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_SUCCESS) + return + } + + if (url.pathname === "/cancel") { + pendingOAuth?.reject(new Error("Login cancelled")) + pendingOAuth = undefined + res.writeHead(200) + res.end("Login cancelled") + return + } + + res.writeHead(404) + res.end("Not found") + }) + + await new Promise((resolve, reject) => { + oauthServer!.listen(OAUTH_PORT, () => { + log.info("codex oauth server started", { port: OAUTH_PORT }) + resolve() + }) + oauthServer!.on("error", reject) }) - log.info("codex oauth server started", { port: OAUTH_PORT }) return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } } function stopOAuthServer() { if (oauthServer) { - oauthServer.stop() + oauthServer.close(() => { + log.info("codex oauth server stopped") + }) oauthServer = undefined - log.info("codex oauth server stopped") } }