summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2026-04-06 12:17:29 -0400
committerGitHub <[email protected]>2026-04-06 12:17:29 -0400
commit2e4c43c1cf6a14c6b2d1d502b70337fae35bc1ce (patch)
tree14c55509a25dce7601fa9b9036e009527b10fb6b
parent965c751522a72f16a523dc86168aadd349765139 (diff)
downloadopencode-2e4c43c1cf6a14c6b2d1d502b70337fae35bc1ce.tar.gz
opencode-2e4c43c1cf6a14c6b2d1d502b70337fae35bc1ce.zip
refactor: replace Bun.serve with Node http.createServer in OAuth handlers (#18327)
Co-authored-by: LukeParkerDev <[email protected]>
-rw-r--r--packages/opencode/src/mcp/oauth-callback.ts161
-rw-r--r--packages/opencode/src/plugin/codex.ts117
2 files changed, 143 insertions, 135 deletions
diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts
index 3a1ca5404..dd1d886fc 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<typeof Bun.serve> | undefined
+ let server: ReturnType<typeof createServer> | undefined
const pendingAuths = new Map<string, PendingAuth>()
// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
// find the right entry in pendingAuths (which is keyed by oauthState).
@@ -60,97 +61,97 @@ export namespace McpOAuthCallback {
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
- export async function ensureRunning(): Promise<void> {
- if (server) return
+ function cleanupStateIndex(oauthState: string) {
+ for (const [name, state] of mcpNameToState) {
+ if (state === oauthState) {
+ mcpNameToState.delete(name)
+ break
+ }
+ }
+ }
- const running = await isPortInUse()
- if (running) {
- log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
+ 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
}
- server = Bun.serve({
- port: OAUTH_CALLBACK_PORT,
- fetch(req) {
- const url = new URL(req.url)
+ 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 !== OAUTH_CALLBACK_PATH) {
- return new Response("Not found", { status: 404 })
- }
+ log.info("received oauth callback", { hasCode: !!code, state, error })
- 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" },
- })
- }
+ // 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)
- 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 (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) {
- return new Response(HTML_ERROR("No authorization code provided"), {
- status: 400,
- headers: { "Content-Type": "text/html" },
- })
- }
+ 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()) })
- return new Response(HTML_ERROR(errorMsg), {
- 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()) })
+ res.writeHead(400, { "Content-Type": "text/html" })
+ res.end(HTML_ERROR(errorMsg))
+ return
+ }
- const pending = pendingAuths.get(state)!
+ 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)
+ clearTimeout(pending.timeout)
+ pendingAuths.delete(state)
+ cleanupStateIndex(state)
+ pending.resolve(code)
- return new Response(HTML_SUCCESS, {
- headers: { "Content-Type": "text/html" },
- })
- },
- })
+ res.writeHead(200, { "Content-Type": "text/html" })
+ res.end(HTML_SUCCESS)
+ }
+
+ export async function ensureRunning(): Promise<void> {
+ if (server) return
+
+ const running = await isPortInUse()
+ if (running) {
+ log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
+ return
+ }
- log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
+ server = createServer(handleRequest)
+ await new Promise<void>((resolve, reject) => {
+ server!.listen(OAUTH_CALLBACK_PORT, () => {
+ log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
+ resolve()
+ })
+ server!.on("error", reject)
+ })
}
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
@@ -196,7 +197,7 @@ export namespace McpOAuthCallback {
export async function stop(): Promise<void> {
if (server) {
- server.stop()
+ await new Promise<void>((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 ee42b9517..77c00eb4f 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<typeof Bun.serve> | undefined
+let oauthServer: ReturnType<typeof createServer> | 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
+ res.writeHead(200, { "Content-Type": "text/html" })
+ res.end(HTML_ERROR(errorMsg))
+ return
+ }
- 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" },
- })
+ if (!code) {
+ const errorMsg = "Missing authorization code"
+ pendingOAuth?.reject(new Error(errorMsg))
+ pendingOAuth = undefined
+ res.writeHead(400, { "Content-Type": "text/html" })
+ res.end(HTML_ERROR(errorMsg))
+ return
}
- if (url.pathname === "/cancel") {
- pendingOAuth?.reject(new Error("Login cancelled"))
+ if (!pendingOAuth || state !== pendingOAuth.state) {
+ const errorMsg = "Invalid state - potential CSRF attack"
+ 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 })
- },
+ 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<void>((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")
}
}