Skip to content

Commit c1eff3d

Browse files
committed
refactor: unwrap McpOAuthCallback namespace + self-reexport
1 parent 23ed876 commit c1eff3d

1 file changed

Lines changed: 143 additions & 143 deletions

File tree

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

Lines changed: 143 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -56,177 +56,177 @@ interface PendingAuth {
5656
timeout: ReturnType<typeof setTimeout>
5757
}
5858

59-
export namespace McpOAuthCallback {
60-
let server: ReturnType<typeof createServer> | undefined
61-
const pendingAuths = new Map<string, PendingAuth>()
62-
// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
63-
// find the right entry in pendingAuths (which is keyed by oauthState).
64-
const mcpNameToState = new Map<string, string>()
65-
66-
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
67-
68-
function cleanupStateIndex(oauthState: string) {
69-
for (const [name, state] of mcpNameToState) {
70-
if (state === oauthState) {
71-
mcpNameToState.delete(name)
72-
break
73-
}
59+
let server: ReturnType<typeof createServer> | undefined
60+
const pendingAuths = new Map<string, PendingAuth>()
61+
// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
62+
// find the right entry in pendingAuths (which is keyed by oauthState).
63+
const mcpNameToState = new Map<string, string>()
64+
65+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
66+
67+
function cleanupStateIndex(oauthState: string) {
68+
for (const [name, state] of mcpNameToState) {
69+
if (state === oauthState) {
70+
mcpNameToState.delete(name)
71+
break
7472
}
7573
}
74+
}
7675

77-
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
78-
const url = new URL(req.url || "/", `http://localhost:${currentPort}`)
76+
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
77+
const url = new URL(req.url || "/", `http://localhost:${currentPort}`)
7978

80-
if (url.pathname !== currentPath) {
81-
res.writeHead(404)
82-
res.end("Not found")
83-
return
84-
}
79+
if (url.pathname !== currentPath) {
80+
res.writeHead(404)
81+
res.end("Not found")
82+
return
83+
}
8584

86-
const code = url.searchParams.get("code")
87-
const state = url.searchParams.get("state")
88-
const error = url.searchParams.get("error")
89-
const errorDescription = url.searchParams.get("error_description")
85+
const code = url.searchParams.get("code")
86+
const state = url.searchParams.get("state")
87+
const error = url.searchParams.get("error")
88+
const errorDescription = url.searchParams.get("error_description")
9089

91-
log.info("received oauth callback", { hasCode: !!code, state, error })
90+
log.info("received oauth callback", { hasCode: !!code, state, error })
9291

93-
// Enforce state parameter presence
94-
if (!state) {
95-
const errorMsg = "Missing required state parameter - potential CSRF attack"
96-
log.error("oauth callback missing state parameter", { url: url.toString() })
97-
res.writeHead(400, { "Content-Type": "text/html" })
98-
res.end(HTML_ERROR(errorMsg))
99-
return
100-
}
92+
// Enforce state parameter presence
93+
if (!state) {
94+
const errorMsg = "Missing required state parameter - potential CSRF attack"
95+
log.error("oauth callback missing state parameter", { url: url.toString() })
96+
res.writeHead(400, { "Content-Type": "text/html" })
97+
res.end(HTML_ERROR(errorMsg))
98+
return
99+
}
101100

102-
if (error) {
103-
const errorMsg = errorDescription || error
104-
if (pendingAuths.has(state)) {
105-
const pending = pendingAuths.get(state)!
106-
clearTimeout(pending.timeout)
107-
pendingAuths.delete(state)
108-
cleanupStateIndex(state)
109-
pending.reject(new Error(errorMsg))
110-
}
111-
res.writeHead(200, { "Content-Type": "text/html" })
112-
res.end(HTML_ERROR(errorMsg))
113-
return
101+
if (error) {
102+
const errorMsg = errorDescription || error
103+
if (pendingAuths.has(state)) {
104+
const pending = pendingAuths.get(state)!
105+
clearTimeout(pending.timeout)
106+
pendingAuths.delete(state)
107+
cleanupStateIndex(state)
108+
pending.reject(new Error(errorMsg))
114109
}
110+
res.writeHead(200, { "Content-Type": "text/html" })
111+
res.end(HTML_ERROR(errorMsg))
112+
return
113+
}
115114

116-
if (!code) {
117-
res.writeHead(400, { "Content-Type": "text/html" })
118-
res.end(HTML_ERROR("No authorization code provided"))
119-
return
120-
}
115+
if (!code) {
116+
res.writeHead(400, { "Content-Type": "text/html" })
117+
res.end(HTML_ERROR("No authorization code provided"))
118+
return
119+
}
121120

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

131-
const pending = pendingAuths.get(state)!
130+
const pending = pendingAuths.get(state)!
132131

133-
clearTimeout(pending.timeout)
134-
pendingAuths.delete(state)
135-
cleanupStateIndex(state)
136-
pending.resolve(code)
132+
clearTimeout(pending.timeout)
133+
pendingAuths.delete(state)
134+
cleanupStateIndex(state)
135+
pending.resolve(code)
137136

138-
res.writeHead(200, { "Content-Type": "text/html" })
139-
res.end(HTML_SUCCESS)
140-
}
137+
res.writeHead(200, { "Content-Type": "text/html" })
138+
res.end(HTML_SUCCESS)
139+
}
141140

142-
export async function ensureRunning(redirectUri?: string): Promise<void> {
143-
// Parse the redirect URI to get port and path (uses defaults if not provided)
144-
const { port, path } = parseRedirectUri(redirectUri)
141+
export async function ensureRunning(redirectUri?: string): Promise<void> {
142+
// Parse the redirect URI to get port and path (uses defaults if not provided)
143+
const { port, path } = parseRedirectUri(redirectUri)
145144

146-
// If server is running on a different port/path, stop it first
147-
if (server && (currentPort !== port || currentPath !== path)) {
148-
log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port })
149-
await stop()
150-
}
145+
// If server is running on a different port/path, stop it first
146+
if (server && (currentPort !== port || currentPath !== path)) {
147+
log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port })
148+
await stop()
149+
}
151150

152-
if (server) return
151+
if (server) return
153152

154-
const running = await isPortInUse(port)
155-
if (running) {
156-
log.info("oauth callback server already running on another instance", { port })
157-
return
158-
}
153+
const running = await isPortInUse(port)
154+
if (running) {
155+
log.info("oauth callback server already running on another instance", { port })
156+
return
157+
}
159158

160-
currentPort = port
161-
currentPath = path
159+
currentPort = port
160+
currentPath = path
162161

163-
server = createServer(handleRequest)
164-
await new Promise<void>((resolve, reject) => {
165-
server!.listen(currentPort, () => {
166-
log.info("oauth callback server started", { port: currentPort, path: currentPath })
167-
resolve()
168-
})
169-
server!.on("error", reject)
162+
server = createServer(handleRequest)
163+
await new Promise<void>((resolve, reject) => {
164+
server!.listen(currentPort, () => {
165+
log.info("oauth callback server started", { port: currentPort, path: currentPath })
166+
resolve()
170167
})
171-
}
168+
server!.on("error", reject)
169+
})
170+
}
172171

173-
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
174-
if (mcpName) mcpNameToState.set(mcpName, oauthState)
175-
return new Promise((resolve, reject) => {
176-
const timeout = setTimeout(() => {
177-
if (pendingAuths.has(oauthState)) {
178-
pendingAuths.delete(oauthState)
179-
if (mcpName) mcpNameToState.delete(mcpName)
180-
reject(new Error("OAuth callback timeout - authorization took too long"))
181-
}
182-
}, CALLBACK_TIMEOUT_MS)
183-
184-
pendingAuths.set(oauthState, { resolve, reject, timeout })
185-
})
186-
}
172+
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
173+
if (mcpName) mcpNameToState.set(mcpName, oauthState)
174+
return new Promise((resolve, reject) => {
175+
const timeout = setTimeout(() => {
176+
if (pendingAuths.has(oauthState)) {
177+
pendingAuths.delete(oauthState)
178+
if (mcpName) mcpNameToState.delete(mcpName)
179+
reject(new Error("OAuth callback timeout - authorization took too long"))
180+
}
181+
}, CALLBACK_TIMEOUT_MS)
187182

188-
export function cancelPending(mcpName: string): void {
189-
// Look up the oauthState for this mcpName via the reverse index
190-
const oauthState = mcpNameToState.get(mcpName)
191-
const key = oauthState ?? mcpName
192-
const pending = pendingAuths.get(key)
193-
if (pending) {
194-
clearTimeout(pending.timeout)
195-
pendingAuths.delete(key)
196-
mcpNameToState.delete(mcpName)
197-
pending.reject(new Error("Authorization cancelled"))
198-
}
199-
}
183+
pendingAuths.set(oauthState, { resolve, reject, timeout })
184+
})
185+
}
200186

201-
export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise<boolean> {
202-
return new Promise((resolve) => {
203-
const socket = createConnection(port, "127.0.0.1")
204-
socket.on("connect", () => {
205-
socket.destroy()
206-
resolve(true)
207-
})
208-
socket.on("error", () => {
209-
resolve(false)
210-
})
211-
})
187+
export function cancelPending(mcpName: string): void {
188+
// Look up the oauthState for this mcpName via the reverse index
189+
const oauthState = mcpNameToState.get(mcpName)
190+
const key = oauthState ?? mcpName
191+
const pending = pendingAuths.get(key)
192+
if (pending) {
193+
clearTimeout(pending.timeout)
194+
pendingAuths.delete(key)
195+
mcpNameToState.delete(mcpName)
196+
pending.reject(new Error("Authorization cancelled"))
212197
}
198+
}
213199

214-
export async function stop(): Promise<void> {
215-
if (server) {
216-
await new Promise<void>((resolve) => server!.close(() => resolve()))
217-
server = undefined
218-
log.info("oauth callback server stopped")
219-
}
200+
export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise<boolean> {
201+
return new Promise((resolve) => {
202+
const socket = createConnection(port, "127.0.0.1")
203+
socket.on("connect", () => {
204+
socket.destroy()
205+
resolve(true)
206+
})
207+
socket.on("error", () => {
208+
resolve(false)
209+
})
210+
})
211+
}
220212

221-
for (const [_name, pending] of pendingAuths) {
222-
clearTimeout(pending.timeout)
223-
pending.reject(new Error("OAuth callback server stopped"))
224-
}
225-
pendingAuths.clear()
226-
mcpNameToState.clear()
213+
export async function stop(): Promise<void> {
214+
if (server) {
215+
await new Promise<void>((resolve) => server!.close(() => resolve()))
216+
server = undefined
217+
log.info("oauth callback server stopped")
227218
}
228219

229-
export function isRunning(): boolean {
230-
return server !== undefined
220+
for (const [_name, pending] of pendingAuths) {
221+
clearTimeout(pending.timeout)
222+
pending.reject(new Error("OAuth callback server stopped"))
231223
}
224+
pendingAuths.clear()
225+
mcpNameToState.clear()
232226
}
227+
228+
export function isRunning(): boolean {
229+
return server !== undefined
230+
}
231+
232+
export * as McpOAuthCallback from "./oauth-callback"

0 commit comments

Comments
 (0)