@@ -12,45 +12,55 @@ import type {
1212 TaskStatusUpdateEvent ,
1313 TaskArtifactUpdateEvent ,
1414} from '@a2a-js/sdk' ;
15+ import type { AuthenticationHandler , Client } from '@a2a-js/sdk/client' ;
1516import {
1617 ClientFactory ,
1718 ClientFactoryOptions ,
1819 DefaultAgentCardResolver ,
19- RestTransportFactory ,
2020 JsonRpcTransportFactory ,
21- type AuthenticationHandler ,
21+ RestTransportFactory ,
2222 createAuthenticatingFetchWithRetry ,
23- type Client ,
2423} from '@a2a-js/sdk/client' ;
2524import { GrpcTransportFactory } from '@a2a-js/sdk/client/grpc' ;
2625import { v4 as uuidv4 } from 'uuid' ;
2726import { Agent as UndiciAgent } from 'undici' ;
28- import { getGrpcCredentials , normalizeAgentCard } from './a2aUtils.js' ;
29- import { isPrivateIpAsync } from '../utils/fetch.js' ;
27+ import {
28+ getGrpcChannelOptions ,
29+ getGrpcCredentials ,
30+ normalizeAgentCard ,
31+ pinUrlToIp ,
32+ } from './a2aUtils.js' ;
33+ import { isPrivateIpAsync , safeLookup } from '../utils/fetch.js' ;
3034import { debugLogger } from '../utils/debugLogger.js' ;
3135
36+ /**
37+ * Result of sending a message, which can be a full message, a task,
38+ * or an incremental status/artifact update.
39+ */
40+ export type SendMessageResult =
41+ | Message
42+ | Task
43+ | TaskStatusUpdateEvent
44+ | TaskArtifactUpdateEvent ;
45+
3246// Remote agents can take 10+ minutes (e.g. Deep Research).
3347// Use a dedicated dispatcher so the global 5-min timeout isn't affected.
3448const A2A_TIMEOUT = 1800000 ; // 30 minutes
3549const a2aDispatcher = new UndiciAgent ( {
3650 headersTimeout : A2A_TIMEOUT ,
3751 bodyTimeout : A2A_TIMEOUT ,
52+ connect : {
53+ // SSRF protection at the connection level (mitigates DNS rebinding)
54+ lookup : safeLookup ,
55+ } ,
3856} ) ;
3957const a2aFetch : typeof fetch = ( input , init ) =>
4058 // @ts -expect-error The `dispatcher` property is a Node.js extension to fetch not present in standard types.
4159 fetch ( input , { ...init , dispatcher : a2aDispatcher } ) ;
4260
43- export type SendMessageResult =
44- | Message
45- | Task
46- | TaskStatusUpdateEvent
47- | TaskArtifactUpdateEvent ;
48-
4961/**
50- * Orchestrates communication with A2A agents.
51- *
52- * This manager handles agent discovery, card caching, and client lifecycle.
53- * It provides a unified messaging interface using the standard A2A SDK.
62+ * Orchestrates communication with remote A2A agents.
63+ * Manages protocol negotiation, authentication, and transport selection.
5464 */
5565export class A2AClientManager {
5666 private static instance : A2AClientManager ;
@@ -83,7 +93,7 @@ export class A2AClientManager {
8393 /**
8494 * Loads an agent by fetching its AgentCard and caches the client.
8595 * @param name The name to assign to the agent.
86- * @param agentCardUrl The full URL to the agent's card.
96+ * @param agentCardUrl {string} The full URL to the agent's card.
8797 * @param authHandler Optional authentication handler to use for this agent.
8898 * @returns The loaded AgentCard.
8999 */
@@ -100,16 +110,26 @@ export class A2AClientManager {
100110 const resolver = new DefaultAgentCardResolver ( { fetchImpl } ) ;
101111 const agentCard = await this . resolveAgentCard ( name , agentCardUrl , resolver ) ;
102112
113+ // Pin URL to IP to prevent DNS rebinding for gRPC (connection-level SSRF protection)
114+ const { pinnedUrl, hostname } = await pinUrlToIp ( agentCard . url , name ) ;
115+
103116 // Configure standard SDK client for tool registration and discovery
104117 const clientOptions = ClientFactoryOptions . createFrom (
105118 ClientFactoryOptions . default ,
106119 {
107120 transports : [
108121 new RestTransportFactory ( { fetchImpl } ) ,
109122 new JsonRpcTransportFactory ( { fetchImpl } ) ,
110- new GrpcTransportFactory ( {
111- grpcChannelCredentials : getGrpcCredentials ( agentCard . url ) ,
112- } ) ,
123+ new GrpcTransportFactory (
124+ // gRPC transport options extension is supported by runtime but missing in SDK types
125+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
126+ {
127+ target : pinnedUrl ,
128+ grpcChannelCredentials : getGrpcCredentials ( agentCard . url ) ,
129+ grpcChannelOptions : getGrpcChannelOptions ( hostname ) ,
130+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131+ } as any ,
132+ ) ,
113133 ] ,
114134 cardResolver : resolver ,
115135 } ,
@@ -166,7 +186,7 @@ export class A2AClientManager {
166186 try {
167187 yield * client . sendMessageStream ( messageParams , {
168188 signal : options ?. signal ,
169- } ) ;
189+ } ) as AsyncIterable < SendMessageResult > ;
170190 } catch ( error : unknown ) {
171191 const prefix = `[A2AClientManager] sendMessageStream Error [${ agentName } ]` ;
172192 if ( error instanceof Error ) {
@@ -275,7 +295,14 @@ export class A2AClientManager {
275295 if ( parsedUrl . pathname . endsWith ( standardPath ) ) {
276296 // Correctly split the URL into baseUrl and standard path
277297 path = standardPath ;
278- baseUrl = url . substring ( 0 , url . lastIndexOf ( standardPath ) ) ;
298+ // Reconstruct baseUrl from parsed components to avoid issues with hashes or query params.
299+ parsedUrl . pathname = parsedUrl . pathname . substring (
300+ 0 ,
301+ parsedUrl . pathname . lastIndexOf ( standardPath ) ,
302+ ) ;
303+ parsedUrl . search = '' ;
304+ parsedUrl . hash = '' ;
305+ baseUrl = parsedUrl . toString ( ) ;
279306 }
280307 } catch ( e ) {
281308 throw new Error ( `Invalid agent card URL: ${ url } ` , { cause : e } ) ;
0 commit comments