Kaynağa Gözat

refactor: replace Bun.serve with Node http.createServer in OAuth handlers (#18327)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Dax 4 ay önce
ebeveyn
işleme
2e4c43c1cf

+ 81 - 80
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")
     }

+ 62 - 55
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")
   }
 }