Skip to content

Commit 8e812f2

Browse files
Apply PR #18327: refactor: replace Bun.serve with Node http.createServer in OAuth handlers
2 parents 491e1d8 + 9f5713e commit 8e812f2

File tree

2 files changed

+132
-122
lines changed

2 files changed

+132
-122
lines changed

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

Lines changed: 70 additions & 67 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,89 +53,91 @@ 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

5859
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
5960

60-
export async function ensureRunning(): Promise<void> {
61-
if (server) return
61+
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
62+
const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`)
6263

63-
const running = await isPortInUse()
64-
if (running) {
65-
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
64+
if (url.pathname !== OAUTH_CALLBACK_PATH) {
65+
res.writeHead(404)
66+
res.end("Not found")
6667
return
6768
}
6869

69-
server = Bun.serve({
70-
port: OAUTH_CALLBACK_PORT,
71-
fetch(req) {
72-
const url = new URL(req.url)
70+
const code = url.searchParams.get("code")
71+
const state = url.searchParams.get("state")
72+
const error = url.searchParams.get("error")
73+
const errorDescription = url.searchParams.get("error_description")
7374

74-
if (url.pathname !== OAUTH_CALLBACK_PATH) {
75-
return new Response("Not found", { status: 404 })
76-
}
75+
log.info("received oauth callback", { hasCode: !!code, state, error })
7776

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

95-
if (error) {
96-
const errorMsg = errorDescription || error
97-
if (pendingAuths.has(state)) {
98-
const pending = pendingAuths.get(state)!
99-
clearTimeout(pending.timeout)
100-
pendingAuths.delete(state)
101-
pending.reject(new Error(errorMsg))
102-
}
103-
return new Response(HTML_ERROR(errorMsg), {
104-
headers: { "Content-Type": "text/html" },
105-
})
106-
}
86+
if (error) {
87+
const errorMsg = errorDescription || error
88+
if (pendingAuths.has(state)) {
89+
const pending = pendingAuths.get(state)!
90+
clearTimeout(pending.timeout)
91+
pendingAuths.delete(state)
92+
pending.reject(new Error(errorMsg))
93+
}
94+
res.writeHead(200, { "Content-Type": "text/html" })
95+
res.end(HTML_ERROR(errorMsg))
96+
return
97+
}
10798

108-
if (!code) {
109-
return new Response(HTML_ERROR("No authorization code provided"), {
110-
status: 400,
111-
headers: { "Content-Type": "text/html" },
112-
})
113-
}
99+
if (!code) {
100+
res.writeHead(400, { "Content-Type": "text/html" })
101+
res.end(HTML_ERROR("No authorization code provided"))
102+
return
103+
}
114104

115-
// Validate state parameter
116-
if (!pendingAuths.has(state)) {
117-
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
118-
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
119-
return new Response(HTML_ERROR(errorMsg), {
120-
status: 400,
121-
headers: { "Content-Type": "text/html" },
122-
})
123-
}
105+
// Validate state parameter
106+
if (!pendingAuths.has(state)) {
107+
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
108+
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
109+
res.writeHead(400, { "Content-Type": "text/html" })
110+
res.end(HTML_ERROR(errorMsg))
111+
return
112+
}
124113

125-
const pending = pendingAuths.get(state)!
114+
const pending = pendingAuths.get(state)!
126115

127-
clearTimeout(pending.timeout)
128-
pendingAuths.delete(state)
129-
pending.resolve(code)
116+
clearTimeout(pending.timeout)
117+
pendingAuths.delete(state)
118+
pending.resolve(code)
130119

131-
return new Response(HTML_SUCCESS, {
132-
headers: { "Content-Type": "text/html" },
133-
})
134-
},
135-
})
120+
res.writeHead(200, { "Content-Type": "text/html" })
121+
res.end(HTML_SUCCESS)
122+
}
123+
124+
export async function ensureRunning(): Promise<void> {
125+
if (server) return
136126

137-
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
127+
const running = await isPortInUse()
128+
if (running) {
129+
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
130+
return
131+
}
132+
133+
server = createServer(handleRequest)
134+
await new Promise<void>((resolve, reject) => {
135+
server!.listen(OAUTH_CALLBACK_PORT, () => {
136+
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
137+
resolve()
138+
})
139+
server!.on("error", reject)
140+
})
138141
}
139142

140143
export function waitForCallback(oauthState: string): Promise<string> {
@@ -174,7 +177,7 @@ export namespace McpOAuthCallback {
174177

175178
export async function stop(): Promise<void> {
176179
if (server) {
177-
server.stop()
180+
await new Promise<void>((resolve) => server!.close(() => resolve()))
178181
server = undefined
179182
log.info("oauth callback server stopped")
180183
}

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)