refactor(mcp-oauth): replace Bun.serve with node:http in callback-server
The OAuth callback server was using Bun.serve, which would crash if mcp-oauth code paths ever entered the plugin bundle. Switch to node:http.createServer with the same WHATWG behavior: - Binds to 127.0.0.1, preserves 200/400/404 status codes - Translates fetch(Request)→Response to (req, res) callback style - Clears OAuth timeout on success/error/missing-param paths - Replaces server.stop(true) with server.close() for shutdown Functional behavior and response bodies unchanged.
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
|
||||||
|
|
||||||
import { findAvailablePort as findAvailablePortShared } from "../../shared/port-utils"
|
import { findAvailablePort as findAvailablePortShared } from "../../shared/port-utils"
|
||||||
|
|
||||||
const DEFAULT_PORT = 19877
|
const DEFAULT_PORT = 19877
|
||||||
@@ -51,56 +53,74 @@ export async function startCallbackServer(startPort: number = DEFAULT_PORT): Pro
|
|||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
rejectCallback?.(new Error("OAuth callback timed out after 5 minutes"))
|
rejectCallback?.(new Error("OAuth callback timed out after 5 minutes"))
|
||||||
server.stop(true)
|
server.close()
|
||||||
}, TIMEOUT_MS)
|
}, TIMEOUT_MS)
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
|
||||||
port: requestedPort,
|
const host = request.headers.host ?? "127.0.0.1"
|
||||||
hostname: "127.0.0.1",
|
const url = new URL(request.url ?? "/", `http://${host}`)
|
||||||
fetch(request: Request): Response {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
|
|
||||||
if (url.pathname !== "/oauth/callback") {
|
if (url.pathname !== "/oauth/callback") {
|
||||||
return new Response("Not Found", { status: 404 })
|
response.statusCode = 404
|
||||||
}
|
response.end("Not Found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const oauthError = url.searchParams.get("error")
|
const oauthError = url.searchParams.get("error")
|
||||||
if (oauthError) {
|
if (oauthError) {
|
||||||
const description = url.searchParams.get("error_description") ?? oauthError
|
const description = url.searchParams.get("error_description") ?? oauthError
|
||||||
clearTimeout(timeoutId)
|
|
||||||
rejectCallback?.(new Error(`OAuth authorization failed: ${description}`))
|
|
||||||
setTimeout(() => server.stop(true), 100)
|
|
||||||
return new Response(`Authorization failed: ${description}`, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = url.searchParams.get("code")
|
|
||||||
const state = url.searchParams.get("state")
|
|
||||||
|
|
||||||
if (!code || !state) {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
rejectCallback?.(new Error("OAuth callback missing code or state parameter"))
|
|
||||||
setTimeout(() => server.stop(true), 100)
|
|
||||||
return new Response("Missing code or state parameter", { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveCallback?.({ code, state })
|
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
rejectCallback?.(new Error(`OAuth authorization failed: ${description}`))
|
||||||
|
response.statusCode = 400
|
||||||
|
response.end(`Authorization failed: ${description}`)
|
||||||
|
setTimeout(() => server.close(), 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => server.stop(true), 100)
|
const code = url.searchParams.get("code")
|
||||||
|
const state = url.searchParams.get("state")
|
||||||
|
|
||||||
return new Response(SUCCESS_HTML, {
|
if (!code || !state) {
|
||||||
headers: { "content-type": "text/html; charset=utf-8" },
|
clearTimeout(timeoutId)
|
||||||
})
|
rejectCallback?.(new Error("OAuth callback missing code or state parameter"))
|
||||||
},
|
response.statusCode = 400
|
||||||
|
response.end("Missing code or state parameter")
|
||||||
|
setTimeout(() => server.close(), 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveCallback?.({ code, state })
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
|
response.statusCode = 200
|
||||||
|
response.setHeader("content-type", "text/html; charset=utf-8")
|
||||||
|
response.end(SUCCESS_HTML)
|
||||||
|
setTimeout(() => server.close(), 100)
|
||||||
})
|
})
|
||||||
const activePort = server.port ?? requestedPort
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const handleError = (error: Error): void => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.once("error", handleError)
|
||||||
|
server.once("listening", () => {
|
||||||
|
server.off("error", handleError)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
server.listen(requestedPort, "127.0.0.1")
|
||||||
|
})
|
||||||
|
|
||||||
|
const address = server.address()
|
||||||
|
const activePort = typeof address === "object" && address !== null ? address.port : requestedPort
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port: activePort,
|
port: activePort,
|
||||||
waitForCallback: () => callbackPromise,
|
waitForCallback: () => callbackPromise,
|
||||||
close: () => {
|
close: () => {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
server.stop(true)
|
server.close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user