mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 18:55:37 -04:00
fix(mcp): cancel timed out oauth refresh
This commit is contained in:
@@ -230,11 +230,15 @@ export const layer = Layer.effect(
|
||||
auth,
|
||||
)
|
||||
authProvider = provider
|
||||
const controller = new AbortController()
|
||||
yield* Effect.tryPromise(() =>
|
||||
withTimeout(
|
||||
provider.refreshTokensIfExpired(mcp.headers ? createFetchWithInit(fetch, { headers: mcp.headers }) : undefined),
|
||||
provider.refreshTokensIfExpired(
|
||||
mcp.headers ? createFetchWithInit(fetch, { headers: mcp.headers }) : undefined,
|
||||
controller.signal,
|
||||
),
|
||||
connectTimeout,
|
||||
),
|
||||
).finally(() => controller.abort()),
|
||||
).pipe(Effect.ignore)
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
)
|
||||
}
|
||||
|
||||
async refreshTokensIfExpired(fetchFn?: FetchLike): Promise<boolean> {
|
||||
async refreshTokensIfExpired(fetchFn?: FetchLike, signal?: AbortSignal): Promise<boolean> {
|
||||
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
|
||||
if (!entry?.tokens?.refreshToken) return false
|
||||
if (!entry.tokens.expiresAt) return false
|
||||
@@ -137,14 +137,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
const clientInformation = await this.clientInformation()
|
||||
if (!clientInformation) return false
|
||||
|
||||
const info = await discoverOAuthServerInfo(this.serverUrl, { fetchFn })
|
||||
const request = signal
|
||||
? (url: string | URL, init?: RequestInit) => (fetchFn ?? fetch)(url, { ...init, signal })
|
||||
: fetchFn
|
||||
const info = await discoverOAuthServerInfo(this.serverUrl, { fetchFn: request })
|
||||
await this.saveTokens(
|
||||
await refreshAuthorization(info.authorizationServerUrl, {
|
||||
metadata: info.authorizationServerMetadata,
|
||||
clientInformation,
|
||||
refreshToken: entry.tokens.refreshToken,
|
||||
resource: await selectResourceURL(this.serverUrl, this, info.resourceMetadata),
|
||||
fetchFn,
|
||||
fetchFn: request,
|
||||
}),
|
||||
)
|
||||
return true
|
||||
|
||||
@@ -54,6 +54,7 @@ let transportCloseCount = 0
|
||||
// Captures the opts passed to each MockStdioTransport, keyed by lastCreatedClientName
|
||||
const stdioOptsByName = new Map<string, any>()
|
||||
let refreshAuthorizationCalls = 0
|
||||
let refreshAborted = false
|
||||
|
||||
function getOrCreateClientState(name?: string): MockClientState {
|
||||
const key = name ?? "default"
|
||||
@@ -253,6 +254,7 @@ beforeEach(() => {
|
||||
clientCreateCount = 0
|
||||
transportCloseCount = 0
|
||||
refreshAuthorizationCalls = 0
|
||||
refreshAborted = false
|
||||
})
|
||||
|
||||
// Import after mocks
|
||||
@@ -307,14 +309,23 @@ it.live("McpOAuthProvider refreshes expired stored tokens", () =>
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"remote connect bounds expired token refresh by mcp timeout",
|
||||
"remote connect cancels expired token refresh after mcp timeout",
|
||||
() =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const original = McpOAuthProvider.prototype.refreshTokensIfExpired
|
||||
McpOAuthProvider.prototype.refreshTokensIfExpired = () => {
|
||||
McpOAuthProvider.prototype.refreshTokensIfExpired = (_fetchFn, signal) => {
|
||||
refreshAuthorizationCalls++
|
||||
return new Promise(() => {})
|
||||
return new Promise((_, reject) => {
|
||||
signal?.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
refreshAborted = true
|
||||
reject(signal.reason)
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
})
|
||||
}
|
||||
return original
|
||||
}),
|
||||
@@ -331,6 +342,7 @@ it.instance(
|
||||
|
||||
expect(statusName(result.status, "remote-timeout")).toBe("connected")
|
||||
expect(refreshAuthorizationCalls).toBe(1)
|
||||
expect(refreshAborted).toBe(true)
|
||||
}),
|
||||
),
|
||||
(original) =>
|
||||
|
||||
Reference in New Issue
Block a user