(mcpServerId: string)
| 17 | * Never throws — revocation is advisory and must not block disconnect/delete flows. |
| 18 | */ |
| 19 | export async function revokeMcpOauthTokens(mcpServerId: string): Promise<void> { |
| 20 | try { |
| 21 | const row = await loadOauthRow({ mcpServerId }) |
| 22 | if (!row?.tokens) return |
| 23 | |
| 24 | const [server] = await db |
| 25 | .select({ |
| 26 | url: mcpServers.url, |
| 27 | oauthClientId: mcpServers.oauthClientId, |
| 28 | oauthClientSecret: mcpServers.oauthClientSecret, |
| 29 | }) |
| 30 | .from(mcpServers) |
| 31 | .where(eq(mcpServers.id, mcpServerId)) |
| 32 | .limit(1) |
| 33 | if (!server?.url) return |
| 34 | |
| 35 | const ssrfGuardedFetch = createSsrfGuardedMcpFetch() |
| 36 | const info = await discoverOAuthServerInfo(server.url, { fetchFn: ssrfGuardedFetch }).catch( |
| 37 | () => undefined |
| 38 | ) |
| 39 | const metadata = info?.authorizationServerMetadata as |
| 40 | | (Record<string, unknown> & { revocation_endpoint?: string }) |
| 41 | | undefined |
| 42 | const revocationEndpoint = metadata?.revocation_endpoint |
| 43 | if (!revocationEndpoint) return |
| 44 | |
| 45 | const clientInfo = row.clientInformation |
| 46 | const clientId = clientInfo?.client_id ?? server.oauthClientId ?? undefined |
| 47 | if (!clientId) return |
| 48 | |
| 49 | let clientSecret = clientInfo?.client_secret |
| 50 | if (!clientSecret && server.oauthClientSecret) { |
| 51 | try { |
| 52 | const { decrypted } = await decryptSecret(server.oauthClientSecret) |
| 53 | clientSecret = decrypted |
| 54 | } catch { |
| 55 | clientSecret = undefined |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | const tokensToRevoke: Array<{ token: string; hint: 'refresh_token' | 'access_token' }> = [] |
| 60 | if (row.tokens.refresh_token) { |
| 61 | tokensToRevoke.push({ token: row.tokens.refresh_token, hint: 'refresh_token' }) |
| 62 | } |
| 63 | if (row.tokens.access_token) { |
| 64 | tokensToRevoke.push({ token: row.tokens.access_token, hint: 'access_token' }) |
| 65 | } |
| 66 | |
| 67 | for (const { token, hint } of tokensToRevoke) { |
| 68 | await postRevoke(revocationEndpoint, token, hint, clientId, clientSecret, ssrfGuardedFetch) |
| 69 | } |
| 70 | } catch (error) { |
| 71 | logger.warn(`Token revocation failed for server ${mcpServerId}`, { |
| 72 | error: toError(error).message, |
| 73 | }) |
| 74 | } |
| 75 | } |
| 76 |
no test coverage detected