Skip to content

Commit 2e4c43c

Browse files
thdxrHona
andauthored
refactor: replace Bun.serve with Node http.createServer in OAuth handlers (anomalyco#18327)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
1 parent 965c751 commit 2e4c43c

2 files changed

Lines changed: 143 additions & 135 deletions

File tree

packages/opencode/src/mcp/oauth-callback.ts

Lines changed: 81 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createConnection } from "net"
2+
import { createServer } from "http"
23
import { Log } from "../util/log"
34
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
45

@@ -52,105 +53,105 @@ interface PendingAuth {
5253
}
5354

5455
export namespace McpOAuthCallback {
55-
let server: ReturnType<typeof Bun.serve> | undefined
56+
let server: ReturnType<typeof createServer> | undefined
5657
const pendingAuths = new Map<string, PendingAuth>()
5758
// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
5859
// find the right entry in pendingAuths (which is keyed by oauthState).
5960
const mcpNameToState = new Map<string, string>()
6061

6162
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
6263

63-
export async function ensureRunning(): Promise<void> {
64-
if (server) return
64+
function cleanupStateIndex(oauthState: string) {
65+
for (const [name, state] of mcpNameToState) {
66+
if (state === oauthState) {
67+
mcpNameToState.delete(name)
68+
break
69+
}
70+
}
71+
}
6572

66-
const running = await isPortInUse()
67-
if (running) {
68-
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
73+
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
74+
const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`)
75+
76+
if (url.pathname !== OAUTH_CALLBACK_PATH) {
77+
res.writeHead(404)
78+
res.end("Not found")
6979
return
7080
}
7181

72-
server = Bun.serve({
73-
port: OAUTH_CALLBACK_PORT,
74-
fetch(req) {
75-
const url = new URL(req.url)
82+
const code = url.searchParams.get("code")
83+
const state = url.searchParams.get("state")
84+
const error = url.searchParams.get("error")
85+
const errorDescription = url.searchParams.get("error_description")
7686

77-
if (url.pathname !== OAUTH_CALLBACK_PATH) {
78-
return new Response("Not found", { status: 404 })
79-
}
87+
log.info("received oauth callback", { hasCode: !!code, state, error })
8088

81-
const code = url.searchParams.get("code")
82-
const state = url.searchParams.get("state")
83-
const error = url.searchParams.get("error")
84-
const errorDescription = url.searchParams.get("error_description")
85-
86-
log.info("received oauth callback", { hasCode: !!code, state, error })
87-
88-
// Enforce state parameter presence
89-
if (!state) {
90-
const errorMsg = "Missing required state parameter - potential CSRF attack"
91-
log.error("oauth callback missing state parameter", { url: url.toString() })
92-
return new Response(HTML_ERROR(errorMsg), {
93-
status: 400,
94-
headers: { "Content-Type": "text/html" },
95-
})
96-
}
89+
// Enforce state parameter presence
90+
if (!state) {
91+
const errorMsg = "Missing required state parameter - potential CSRF attack"
92+
log.error("oauth callback missing state parameter", { url: url.toString() })
93+
res.writeHead(400, { "Content-Type": "text/html" })
94+
res.end(HTML_ERROR(errorMsg))
95+
return
96+
}
9797

98-
if (error) {
99-
const errorMsg = errorDescription || error
100-
if (pendingAuths.has(state)) {
101-
const pending = pendingAuths.get(state)!
102-
clearTimeout(pending.timeout)
103-
pendingAuths.delete(state)
104-
for (const [name, s] of mcpNameToState) {
105-
if (s === state) {
106-
mcpNameToState.delete(name)
107-
break
108-
}
109-
}
110-
pending.reject(new Error(errorMsg))
111-
}
112-
return new Response(HTML_ERROR(errorMsg), {
113-
headers: { "Content-Type": "text/html" },
114-
})
115-
}
98+
if (error) {
99+
const errorMsg = errorDescription || error
100+
if (pendingAuths.has(state)) {
101+
const pending = pendingAuths.get(state)!
102+
clearTimeout(pending.timeout)
103+
pendingAuths.delete(state)
104+
cleanupStateIndex(state)
105+
pending.reject(new Error(errorMsg))
106+
}
107+
res.writeHead(200, { "Content-Type": "text/html" })
108+
res.end(HTML_ERROR(errorMsg))
109+
return
110+
}
116111

117-
if (!code) {
118-
return new Response(HTML_ERROR("No authorization code provided"), {
119-
status: 400,
120-
headers: { "Content-Type": "text/html" },
121-
})
122-
}
112+
if (!code) {
113+
res.writeHead(400, { "Content-Type": "text/html" })
114+
res.end(HTML_ERROR("No authorization code provided"))
115+
return
116+
}
123117

124-
// Validate state parameter
125-
if (!pendingAuths.has(state)) {
126-
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
127-
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
128-
return new Response(HTML_ERROR(errorMsg), {
129-
status: 400,
130-
headers: { "Content-Type": "text/html" },
131-
})
132-
}
118+
// Validate state parameter
119+
if (!pendingAuths.has(state)) {
120+
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
121+
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
122+
res.writeHead(400, { "Content-Type": "text/html" })
123+
res.end(HTML_ERROR(errorMsg))
124+
return
125+
}
133126

134-
const pending = pendingAuths.get(state)!
127+
const pending = pendingAuths.get(state)!
135128

136-
clearTimeout(pending.timeout)
137-
pendingAuths.delete(state)
138-
// Clean up reverse index
139-
for (const [name, s] of mcpNameToState) {
140-
if (s === state) {
141-
mcpNameToState.delete(name)
142-
break
143-
}
144-
}
145-
pending.resolve(code)
129+
clearTimeout(pending.timeout)
130+
pendingAuths.delete(state)
131+
cleanupStateIndex(state)
132+
pending.resolve(code)
146133

147-
return new Response(HTML_SUCCESS, {
148-
headers: { "Content-Type": "text/html" },
149-
})
150-
},
151-
})
134+
res.writeHead(200, { "Content-Type": "text/html" })
135+
res.end(HTML_SUCCESS)
136+
}
137+
138+
export async function ensureRunning(): Promise<void> {
139+
if (server) return
140+
141+
const running = await isPortInUse()
142+
if (running) {
143+
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
144+
return
145+
}
152146

153-
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
147+
server = createServer(handleRequest)
148+
await new Promise<void>((resolve, reject) => {
149+
server!.listen(OAUTH_CALLBACK_PORT, () => {
150+
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
151+
resolve()
152+
})
153+
server!.on("error", reject)
154+
})
154155
}
155156

156157
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
@@ -196,7 +197,7 @@ export namespace McpOAuthCallback {
196197

197198
export async function stop(): Promise<void> {
198199
if (server) {
199-
server.stop()
200+
await new Promise<void>((resolve) => server!.close(() => resolve()))
200201
server = undefined
201202
log.info("oauth callback server stopped")
202203
}

packages/opencode/src/plugin/codex.ts

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import os from "os"
66
import { ProviderTransform } from "@/provider/transform"
77
import { ModelID, ProviderID } from "@/provider/schema"
88
import { setTimeout as sleep } from "node:timers/promises"
9+
import { createServer } from "http"
910

1011
const log = Log.create({ service: "plugin.codex" })
1112

@@ -241,85 +242,91 @@ interface PendingOAuth {
241242
reject: (error: Error) => void
242243
}
243244

244-
let oauthServer: ReturnType<typeof Bun.serve> | undefined
245+
let oauthServer: ReturnType<typeof createServer> | undefined
245246
let pendingOAuth: PendingOAuth | undefined
246247

247248
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
248249
if (oauthServer) {
249250
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
250251
}
251252

252-
oauthServer = Bun.serve({
253-
port: OAUTH_PORT,
254-
fetch(req) {
255-
const url = new URL(req.url)
253+
oauthServer = createServer((req, res) => {
254+
const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`)
256255

257-
if (url.pathname === "/auth/callback") {
258-
const code = url.searchParams.get("code")
259-
const state = url.searchParams.get("state")
260-
const error = url.searchParams.get("error")
261-
const errorDescription = url.searchParams.get("error_description")
256+
if (url.pathname === "/auth/callback") {
257+
const code = url.searchParams.get("code")
258+
const state = url.searchParams.get("state")
259+
const error = url.searchParams.get("error")
260+
const errorDescription = url.searchParams.get("error_description")
262261

263-
if (error) {
264-
const errorMsg = errorDescription || error
265-
pendingOAuth?.reject(new Error(errorMsg))
266-
pendingOAuth = undefined
267-
return new Response(HTML_ERROR(errorMsg), {
268-
headers: { "Content-Type": "text/html" },
269-
})
270-
}
271-
272-
if (!code) {
273-
const errorMsg = "Missing authorization code"
274-
pendingOAuth?.reject(new Error(errorMsg))
275-
pendingOAuth = undefined
276-
return new Response(HTML_ERROR(errorMsg), {
277-
status: 400,
278-
headers: { "Content-Type": "text/html" },
279-
})
280-
}
281-
282-
if (!pendingOAuth || state !== pendingOAuth.state) {
283-
const errorMsg = "Invalid state - potential CSRF attack"
284-
pendingOAuth?.reject(new Error(errorMsg))
285-
pendingOAuth = undefined
286-
return new Response(HTML_ERROR(errorMsg), {
287-
status: 400,
288-
headers: { "Content-Type": "text/html" },
289-
})
290-
}
291-
292-
const current = pendingOAuth
262+
if (error) {
263+
const errorMsg = errorDescription || error
264+
pendingOAuth?.reject(new Error(errorMsg))
293265
pendingOAuth = undefined
266+
res.writeHead(200, { "Content-Type": "text/html" })
267+
res.end(HTML_ERROR(errorMsg))
268+
return
269+
}
294270

295-
exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
296-
.then((tokens) => current.resolve(tokens))
297-
.catch((err) => current.reject(err))
298-
299-
return new Response(HTML_SUCCESS, {
300-
headers: { "Content-Type": "text/html" },
301-
})
271+
if (!code) {
272+
const errorMsg = "Missing authorization code"
273+
pendingOAuth?.reject(new Error(errorMsg))
274+
pendingOAuth = undefined
275+
res.writeHead(400, { "Content-Type": "text/html" })
276+
res.end(HTML_ERROR(errorMsg))
277+
return
302278
}
303279

304-
if (url.pathname === "/cancel") {
305-
pendingOAuth?.reject(new Error("Login cancelled"))
280+
if (!pendingOAuth || state !== pendingOAuth.state) {
281+
const errorMsg = "Invalid state - potential CSRF attack"
282+
pendingOAuth?.reject(new Error(errorMsg))
306283
pendingOAuth = undefined
307-
return new Response("Login cancelled", { status: 200 })
284+
res.writeHead(400, { "Content-Type": "text/html" })
285+
res.end(HTML_ERROR(errorMsg))
286+
return
308287
}
309288

310-
return new Response("Not found", { status: 404 })
311-
},
289+
const current = pendingOAuth
290+
pendingOAuth = undefined
291+
292+
exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
293+
.then((tokens) => current.resolve(tokens))
294+
.catch((err) => current.reject(err))
295+
296+
res.writeHead(200, { "Content-Type": "text/html" })
297+
res.end(HTML_SUCCESS)
298+
return
299+
}
300+
301+
if (url.pathname === "/cancel") {
302+
pendingOAuth?.reject(new Error("Login cancelled"))
303+
pendingOAuth = undefined
304+
res.writeHead(200)
305+
res.end("Login cancelled")
306+
return
307+
}
308+
309+
res.writeHead(404)
310+
res.end("Not found")
311+
})
312+
313+
await new Promise<void>((resolve, reject) => {
314+
oauthServer!.listen(OAUTH_PORT, () => {
315+
log.info("codex oauth server started", { port: OAUTH_PORT })
316+
resolve()
317+
})
318+
oauthServer!.on("error", reject)
312319
})
313320

314-
log.info("codex oauth server started", { port: OAUTH_PORT })
315321
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
316322
}
317323

318324
function stopOAuthServer() {
319325
if (oauthServer) {
320-
oauthServer.stop()
326+
oauthServer.close(() => {
327+
log.info("codex oauth server stopped")
328+
})
321329
oauthServer = undefined
322-
log.info("codex oauth server stopped")
323330
}
324331
}
325332

0 commit comments

Comments
 (0)